├── .gitattributes ├── .github ├── stale.yml └── workflows │ └── CI.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc ├── .waffle.json ├── .yarnrc ├── LICENSE ├── README.md ├── buildV1 ├── UniswapV1Exchange.json └── UniswapV1Factory.json ├── contracts ├── UniswapV2Migrator.sol ├── UniswapV2Router01.sol ├── UniswapV2Router02.sol ├── examples │ ├── ExampleComputeLiquidityValue.sol │ ├── ExampleFlashSwap.sol │ ├── ExampleOracleSimple.sol │ ├── ExampleSlidingWindowOracle.sol │ ├── ExampleSwapToPrice.sol │ └── README.md ├── interfaces │ ├── IERC20.sol │ ├── IUniswapV2Migrator.sol │ ├── IUniswapV2Router01.sol │ ├── IUniswapV2Router02.sol │ ├── IWETH.sol │ └── V1 │ │ ├── IUniswapV1Exchange.sol │ │ └── IUniswapV1Factory.sol ├── libraries │ ├── SafeMath.sol │ ├── UniswapV2Library.sol │ ├── UniswapV2LiquidityMathLibrary.sol │ └── UniswapV2OracleLibrary.sol └── test │ ├── DeflatingERC20.sol │ ├── ERC20.sol │ ├── RouterEventEmitter.sol │ └── WETH9.sol ├── package.json ├── test ├── ExampleComputeLiquidityValue.spec.ts ├── ExampleFlashSwap.spec.ts ├── ExampleOracleSimple.spec.ts ├── ExampleSlidingWindowOracle.spec.ts ├── ExampleSwapToPrice.spec.ts ├── UniswapV2Migrator.spec.ts ├── UniswapV2Router01.spec.ts ├── UniswapV2Router02.spec.ts └── shared │ ├── fixtures.ts │ └── utilities.ts ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | issues: 4 | # Number of days of inactivity before an Issue or Pull Request becomes stale 5 | daysUntilStale: 7 6 | 7 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 8 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 9 | daysUntilClose: 7 10 | 11 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 12 | onlyLabels: 13 | - question 14 | - autoclose 15 | 16 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 17 | exemptLabels: 18 | - p0 19 | - bug 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as stale because it has not had 24 | recent activity. It will be closed if no further activity occurs. Thank you 25 | for your contributions. 26 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | node: ['10.x', '12.x'] 14 | os: [ubuntu-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - run: npm install -g yarn 25 | 26 | - id: yarn-cache 27 | run: echo "::set-output name=dir::$(yarn cache dir)" 28 | - uses: actions/cache@v1 29 | with: 30 | path: ${{ steps.yarn-cache.outputs.dir }} 31 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 32 | restore-keys: | 33 | ${{ matrix.os }}-yarn- 34 | 35 | - run: yarn 36 | 37 | - run: yarn lint 38 | - run: yarn test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "./test/**/*.spec.ts", 4 | "require": "ts-node/register", 5 | "timeout": 12000 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.waffle.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerVersion": "./node_modules/solc", 3 | "outputType": "all", 4 | "compilerOptions": { 5 | "outputSelection": { 6 | "*": { 7 | "*": [ 8 | "evm.bytecode.object", 9 | "evm.deployedBytecode.object", 10 | "abi", 11 | "evm.bytecode.sourceMap", 12 | "evm.deployedBytecode.sourceMap", 13 | "metadata" 14 | ], 15 | "": ["ast"] 16 | } 17 | }, 18 | "evmVersion": "istanbul", 19 | "optimizer": { 20 | "enabled": true, 21 | "runs": 999999 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap V2 2 | 3 | [![Actions Status](https://github.com/Uniswap/uniswap-v2-periphery/workflows/CI/badge.svg)](https://github.com/Uniswap/uniswap-v2-periphery/actions) 4 | [![npm](https://img.shields.io/npm/v/@uniswap/v2-periphery?style=flat-square)](https://npmjs.com/package/@uniswap/v2-periphery) 5 | 6 | In-depth documentation on Uniswap V2 is available at [uniswap.org](https://uniswap.org/docs). 7 | 8 | The built contract artifacts can be browsed via [unpkg.com](https://unpkg.com/browse/@uniswap/v2-periphery@latest/). 9 | 10 | # Local Development 11 | 12 | The following assumes the use of `node@>=10`. 13 | 14 | ## Install Dependencies 15 | 16 | `yarn` 17 | 18 | ## Compile Contracts 19 | 20 | `yarn compile` 21 | 22 | ## Run Tests 23 | 24 | `yarn test` 25 | -------------------------------------------------------------------------------- /buildV1/UniswapV1Factory.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "name": "NewExchange", 5 | "inputs": [ 6 | { "type": "address", "name": "token", "indexed": true }, 7 | { "type": "address", "name": "exchange", "indexed": true } 8 | ], 9 | "anonymous": false, 10 | "type": "event" 11 | }, 12 | { 13 | "name": "initializeFactory", 14 | "outputs": [], 15 | "inputs": [{ "type": "address", "name": "template" }], 16 | "constant": false, 17 | "payable": false, 18 | "type": "function", 19 | "gas": 35725 20 | }, 21 | { 22 | "name": "createExchange", 23 | "outputs": [{ "type": "address", "name": "out" }], 24 | "inputs": [{ "type": "address", "name": "token" }], 25 | "constant": false, 26 | "payable": false, 27 | "type": "function", 28 | "gas": 187911 29 | }, 30 | { 31 | "name": "getExchange", 32 | "outputs": [{ "type": "address", "name": "out" }], 33 | "inputs": [{ "type": "address", "name": "token" }], 34 | "constant": true, 35 | "payable": false, 36 | "type": "function", 37 | "gas": 715 38 | }, 39 | { 40 | "name": "getToken", 41 | "outputs": [{ "type": "address", "name": "out" }], 42 | "inputs": [{ "type": "address", "name": "exchange" }], 43 | "constant": true, 44 | "payable": false, 45 | "type": "function", 46 | "gas": 745 47 | }, 48 | { 49 | "name": "getTokenWithId", 50 | "outputs": [{ "type": "address", "name": "out" }], 51 | "inputs": [{ "type": "uint256", "name": "token_id" }], 52 | "constant": true, 53 | "payable": false, 54 | "type": "function", 55 | "gas": 736 56 | }, 57 | { 58 | "name": "exchangeTemplate", 59 | "outputs": [{ "type": "address", "name": "out" }], 60 | "inputs": [], 61 | "constant": true, 62 | "payable": false, 63 | "type": "function", 64 | "gas": 633 65 | }, 66 | { 67 | "name": "tokenCount", 68 | "outputs": [{ "type": "uint256", "name": "out" }], 69 | "inputs": [], 70 | "constant": true, 71 | "payable": false, 72 | "type": "function", 73 | "gas": 663 74 | } 75 | ], 76 | "evm": { 77 | "bytecode": { 78 | "object": "6103f056600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffffffffffffdabf41c006080527ffffffffffffffffffffffffed5fa0e000000000000000000000000000000000060a05263538a3f0e60005114156100ed57602060046101403734156100b457600080fd5b60043560205181106100c557600080fd5b50600054156100d357600080fd5b60006101405114156100e457600080fd5b61014051600055005b631648f38e60005114156102bf576020600461014037341561010e57600080fd5b600435602051811061011f57600080fd5b50600061014051141561013157600080fd5b6000600054141561014157600080fd5b60026101405160e05260c052604060c020541561015d57600080fd5b7f602e600c600039602e6000f33660006000376110006000366000730000000000610180526c010000000000000000000000006000540261019b527f5af41558576110006000f30000000000000000000000000000000000000000006101af5260406101806000f0806101cf57600080fd5b61016052610160513b6101e157600080fd5b610160513014156101f157600080fd5b6000600060246366d3820361022052610140516102405261023c6000610160515af161021c57600080fd5b6101605160026101405160e05260c052604060c020556101405160036101605160e05260c052604060c02055600154600160015401101561025c57600080fd5b6001600154016102a0526102a0516001556101405160046102a05160e05260c052604060c0205561016051610140517f9d42cb017eb05bd8944ab536a8b35bc68085931dd5f4356489801453923953f960006000a36101605160005260206000f3005b6306f2bf62600051141561030e57602060046101403734156102e057600080fd5b60043560205181106102f157600080fd5b5060026101405160e05260c052604060c0205460005260206000f3005b6359770438600051141561035d576020600461014037341561032f57600080fd5b600435602051811061034057600080fd5b5060036101405160e05260c052604060c0205460005260206000f3005b63aa65a6c0600051141561039a576020600461014037341561037e57600080fd5b60046101405160e05260c052604060c0205460005260206000f3005b631c2bbd1860005114156103c05734156103b357600080fd5b60005460005260206000f3005b639f181b5e60005114156103e65734156103d957600080fd5b60015460005260206000f3005b60006000fd5b6100046103f0036100046000396100046103f0036000f3" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /contracts/UniswapV2Migrator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; 4 | 5 | import './interfaces/IUniswapV2Migrator.sol'; 6 | import './interfaces/V1/IUniswapV1Factory.sol'; 7 | import './interfaces/V1/IUniswapV1Exchange.sol'; 8 | import './interfaces/IUniswapV2Router01.sol'; 9 | import './interfaces/IERC20.sol'; 10 | 11 | contract UniswapV2Migrator is IUniswapV2Migrator { 12 | IUniswapV1Factory immutable factoryV1; 13 | IUniswapV2Router01 immutable router; 14 | 15 | constructor(address _factoryV1, address _router) public { 16 | factoryV1 = IUniswapV1Factory(_factoryV1); 17 | router = IUniswapV2Router01(_router); 18 | } 19 | 20 | // needs to accept ETH from any v1 exchange and the router. ideally this could be enforced, as in the router, 21 | // but it's not possible because it requires a call to the v1 factory, which takes too much gas 22 | receive() external payable {} 23 | 24 | function migrate(address token, uint amountTokenMin, uint amountETHMin, address to, uint deadline) 25 | external 26 | override 27 | { 28 | IUniswapV1Exchange exchangeV1 = IUniswapV1Exchange(factoryV1.getExchange(token)); 29 | uint liquidityV1 = exchangeV1.balanceOf(msg.sender); 30 | require(exchangeV1.transferFrom(msg.sender, address(this), liquidityV1), 'TRANSFER_FROM_FAILED'); 31 | (uint amountETHV1, uint amountTokenV1) = exchangeV1.removeLiquidity(liquidityV1, 1, 1, uint(-1)); 32 | TransferHelper.safeApprove(token, address(router), amountTokenV1); 33 | (uint amountTokenV2, uint amountETHV2,) = router.addLiquidityETH{value: amountETHV1}( 34 | token, 35 | amountTokenV1, 36 | amountTokenMin, 37 | amountETHMin, 38 | to, 39 | deadline 40 | ); 41 | if (amountTokenV1 > amountTokenV2) { 42 | TransferHelper.safeApprove(token, address(router), 0); // be a good blockchain citizen, reset allowance to 0 43 | TransferHelper.safeTransfer(token, msg.sender, amountTokenV1 - amountTokenV2); 44 | } else if (amountETHV1 > amountETHV2) { 45 | // addLiquidityETH guarantees that all of amountETHV1 or amountTokenV1 will be used, hence this else is safe 46 | TransferHelper.safeTransferETH(msg.sender, amountETHV1 - amountETHV2); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/UniswapV2Router01.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; 4 | import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; 5 | 6 | import './libraries/UniswapV2Library.sol'; 7 | import './interfaces/IUniswapV2Router01.sol'; 8 | import './interfaces/IERC20.sol'; 9 | import './interfaces/IWETH.sol'; 10 | 11 | contract UniswapV2Router01 is IUniswapV2Router01 { 12 | address public immutable override factory; 13 | address public immutable override WETH; 14 | 15 | modifier ensure(uint deadline) { 16 | require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); 17 | _; 18 | } 19 | 20 | constructor(address _factory, address _WETH) public { 21 | factory = _factory; 22 | WETH = _WETH; 23 | } 24 | 25 | receive() external payable { 26 | assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract 27 | } 28 | 29 | // **** ADD LIQUIDITY **** 30 | function _addLiquidity( 31 | address tokenA, 32 | address tokenB, 33 | uint amountADesired, 34 | uint amountBDesired, 35 | uint amountAMin, 36 | uint amountBMin 37 | ) private returns (uint amountA, uint amountB) { 38 | // create the pair if it doesn't exist yet 39 | if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { 40 | IUniswapV2Factory(factory).createPair(tokenA, tokenB); 41 | } 42 | (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 43 | if (reserveA == 0 && reserveB == 0) { 44 | (amountA, amountB) = (amountADesired, amountBDesired); 45 | } else { 46 | uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); 47 | if (amountBOptimal <= amountBDesired) { 48 | require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); 49 | (amountA, amountB) = (amountADesired, amountBOptimal); 50 | } else { 51 | uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); 52 | assert(amountAOptimal <= amountADesired); 53 | require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); 54 | (amountA, amountB) = (amountAOptimal, amountBDesired); 55 | } 56 | } 57 | } 58 | function addLiquidity( 59 | address tokenA, 60 | address tokenB, 61 | uint amountADesired, 62 | uint amountBDesired, 63 | uint amountAMin, 64 | uint amountBMin, 65 | address to, 66 | uint deadline 67 | ) external override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { 68 | (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); 69 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 70 | TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); 71 | TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); 72 | liquidity = IUniswapV2Pair(pair).mint(to); 73 | } 74 | function addLiquidityETH( 75 | address token, 76 | uint amountTokenDesired, 77 | uint amountTokenMin, 78 | uint amountETHMin, 79 | address to, 80 | uint deadline 81 | ) external override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { 82 | (amountToken, amountETH) = _addLiquidity( 83 | token, 84 | WETH, 85 | amountTokenDesired, 86 | msg.value, 87 | amountTokenMin, 88 | amountETHMin 89 | ); 90 | address pair = UniswapV2Library.pairFor(factory, token, WETH); 91 | TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); 92 | IWETH(WETH).deposit{value: amountETH}(); 93 | assert(IWETH(WETH).transfer(pair, amountETH)); 94 | liquidity = IUniswapV2Pair(pair).mint(to); 95 | if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); // refund dust eth, if any 96 | } 97 | 98 | // **** REMOVE LIQUIDITY **** 99 | function removeLiquidity( 100 | address tokenA, 101 | address tokenB, 102 | uint liquidity, 103 | uint amountAMin, 104 | uint amountBMin, 105 | address to, 106 | uint deadline 107 | ) public override ensure(deadline) returns (uint amountA, uint amountB) { 108 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 109 | IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair 110 | (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); 111 | (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); 112 | (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); 113 | require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); 114 | require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); 115 | } 116 | function removeLiquidityETH( 117 | address token, 118 | uint liquidity, 119 | uint amountTokenMin, 120 | uint amountETHMin, 121 | address to, 122 | uint deadline 123 | ) public override ensure(deadline) returns (uint amountToken, uint amountETH) { 124 | (amountToken, amountETH) = removeLiquidity( 125 | token, 126 | WETH, 127 | liquidity, 128 | amountTokenMin, 129 | amountETHMin, 130 | address(this), 131 | deadline 132 | ); 133 | TransferHelper.safeTransfer(token, to, amountToken); 134 | IWETH(WETH).withdraw(amountETH); 135 | TransferHelper.safeTransferETH(to, amountETH); 136 | } 137 | function removeLiquidityWithPermit( 138 | address tokenA, 139 | address tokenB, 140 | uint liquidity, 141 | uint amountAMin, 142 | uint amountBMin, 143 | address to, 144 | uint deadline, 145 | bool approveMax, uint8 v, bytes32 r, bytes32 s 146 | ) external override returns (uint amountA, uint amountB) { 147 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 148 | uint value = approveMax ? uint(-1) : liquidity; 149 | IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); 150 | (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); 151 | } 152 | function removeLiquidityETHWithPermit( 153 | address token, 154 | uint liquidity, 155 | uint amountTokenMin, 156 | uint amountETHMin, 157 | address to, 158 | uint deadline, 159 | bool approveMax, uint8 v, bytes32 r, bytes32 s 160 | ) external override returns (uint amountToken, uint amountETH) { 161 | address pair = UniswapV2Library.pairFor(factory, token, WETH); 162 | uint value = approveMax ? uint(-1) : liquidity; 163 | IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); 164 | (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); 165 | } 166 | 167 | // **** SWAP **** 168 | // requires the initial amount to have already been sent to the first pair 169 | function _swap(uint[] memory amounts, address[] memory path, address _to) private { 170 | for (uint i; i < path.length - 1; i++) { 171 | (address input, address output) = (path[i], path[i + 1]); 172 | (address token0,) = UniswapV2Library.sortTokens(input, output); 173 | uint amountOut = amounts[i + 1]; 174 | (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); 175 | address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; 176 | IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(amount0Out, amount1Out, to, new bytes(0)); 177 | } 178 | } 179 | function swapExactTokensForTokens( 180 | uint amountIn, 181 | uint amountOutMin, 182 | address[] calldata path, 183 | address to, 184 | uint deadline 185 | ) external override ensure(deadline) returns (uint[] memory amounts) { 186 | amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); 187 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 188 | TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); 189 | _swap(amounts, path, to); 190 | } 191 | function swapTokensForExactTokens( 192 | uint amountOut, 193 | uint amountInMax, 194 | address[] calldata path, 195 | address to, 196 | uint deadline 197 | ) external override ensure(deadline) returns (uint[] memory amounts) { 198 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 199 | require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 200 | TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); 201 | _swap(amounts, path, to); 202 | } 203 | function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) 204 | external 205 | override 206 | payable 207 | ensure(deadline) 208 | returns (uint[] memory amounts) 209 | { 210 | require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); 211 | amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); 212 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 213 | IWETH(WETH).deposit{value: amounts[0]}(); 214 | assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); 215 | _swap(amounts, path, to); 216 | } 217 | function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 218 | external 219 | override 220 | ensure(deadline) 221 | returns (uint[] memory amounts) 222 | { 223 | require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); 224 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 225 | require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 226 | TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); 227 | _swap(amounts, path, address(this)); 228 | IWETH(WETH).withdraw(amounts[amounts.length - 1]); 229 | TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); 230 | } 231 | function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 232 | external 233 | override 234 | ensure(deadline) 235 | returns (uint[] memory amounts) 236 | { 237 | require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); 238 | amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); 239 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 240 | TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]); 241 | _swap(amounts, path, address(this)); 242 | IWETH(WETH).withdraw(amounts[amounts.length - 1]); 243 | TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); 244 | } 245 | function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) 246 | external 247 | override 248 | payable 249 | ensure(deadline) 250 | returns (uint[] memory amounts) 251 | { 252 | require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); 253 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 254 | require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 255 | IWETH(WETH).deposit{value: amounts[0]}(); 256 | assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); 257 | _swap(amounts, path, to); 258 | if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); // refund dust eth, if any 259 | } 260 | 261 | function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) { 262 | return UniswapV2Library.quote(amountA, reserveA, reserveB); 263 | } 264 | 265 | function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure override returns (uint amountOut) { 266 | return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); 267 | } 268 | 269 | function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) public pure override returns (uint amountIn) { 270 | return UniswapV2Library.getAmountOut(amountOut, reserveIn, reserveOut); 271 | } 272 | 273 | function getAmountsOut(uint amountIn, address[] memory path) public view override returns (uint[] memory amounts) { 274 | return UniswapV2Library.getAmountsOut(factory, amountIn, path); 275 | } 276 | 277 | function getAmountsIn(uint amountOut, address[] memory path) public view override returns (uint[] memory amounts) { 278 | return UniswapV2Library.getAmountsIn(factory, amountOut, path); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /contracts/UniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; 4 | import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; 5 | 6 | import './interfaces/IUniswapV2Router02.sol'; 7 | import './libraries/UniswapV2Library.sol'; 8 | import './libraries/SafeMath.sol'; 9 | import './interfaces/IERC20.sol'; 10 | import './interfaces/IWETH.sol'; 11 | 12 | contract UniswapV2Router02 is IUniswapV2Router02 { 13 | using SafeMath for uint; 14 | 15 | address public immutable override factory; 16 | address public immutable override WETH; 17 | 18 | modifier ensure(uint deadline) { 19 | require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); 20 | _; 21 | } 22 | 23 | constructor(address _factory, address _WETH) public { 24 | factory = _factory; 25 | WETH = _WETH; 26 | } 27 | 28 | receive() external payable { 29 | assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract 30 | } 31 | 32 | // **** ADD LIQUIDITY **** 33 | function _addLiquidity( 34 | address tokenA, 35 | address tokenB, 36 | uint amountADesired, 37 | uint amountBDesired, 38 | uint amountAMin, 39 | uint amountBMin 40 | ) internal virtual returns (uint amountA, uint amountB) { 41 | // create the pair if it doesn't exist yet 42 | if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { 43 | IUniswapV2Factory(factory).createPair(tokenA, tokenB); 44 | } 45 | (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 46 | if (reserveA == 0 && reserveB == 0) { 47 | (amountA, amountB) = (amountADesired, amountBDesired); 48 | } else { 49 | uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); 50 | if (amountBOptimal <= amountBDesired) { 51 | require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); 52 | (amountA, amountB) = (amountADesired, amountBOptimal); 53 | } else { 54 | uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); 55 | assert(amountAOptimal <= amountADesired); 56 | require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); 57 | (amountA, amountB) = (amountAOptimal, amountBDesired); 58 | } 59 | } 60 | } 61 | function addLiquidity( 62 | address tokenA, 63 | address tokenB, 64 | uint amountADesired, 65 | uint amountBDesired, 66 | uint amountAMin, 67 | uint amountBMin, 68 | address to, 69 | uint deadline 70 | ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { 71 | (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); 72 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 73 | TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); 74 | TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); 75 | liquidity = IUniswapV2Pair(pair).mint(to); 76 | } 77 | function addLiquidityETH( 78 | address token, 79 | uint amountTokenDesired, 80 | uint amountTokenMin, 81 | uint amountETHMin, 82 | address to, 83 | uint deadline 84 | ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) { 85 | (amountToken, amountETH) = _addLiquidity( 86 | token, 87 | WETH, 88 | amountTokenDesired, 89 | msg.value, 90 | amountTokenMin, 91 | amountETHMin 92 | ); 93 | address pair = UniswapV2Library.pairFor(factory, token, WETH); 94 | TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken); 95 | IWETH(WETH).deposit{value: amountETH}(); 96 | assert(IWETH(WETH).transfer(pair, amountETH)); 97 | liquidity = IUniswapV2Pair(pair).mint(to); 98 | // refund dust eth, if any 99 | if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH); 100 | } 101 | 102 | // **** REMOVE LIQUIDITY **** 103 | function removeLiquidity( 104 | address tokenA, 105 | address tokenB, 106 | uint liquidity, 107 | uint amountAMin, 108 | uint amountBMin, 109 | address to, 110 | uint deadline 111 | ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { 112 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 113 | IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair 114 | (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); 115 | (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); 116 | (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); 117 | require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); 118 | require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); 119 | } 120 | function removeLiquidityETH( 121 | address token, 122 | uint liquidity, 123 | uint amountTokenMin, 124 | uint amountETHMin, 125 | address to, 126 | uint deadline 127 | ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) { 128 | (amountToken, amountETH) = removeLiquidity( 129 | token, 130 | WETH, 131 | liquidity, 132 | amountTokenMin, 133 | amountETHMin, 134 | address(this), 135 | deadline 136 | ); 137 | TransferHelper.safeTransfer(token, to, amountToken); 138 | IWETH(WETH).withdraw(amountETH); 139 | TransferHelper.safeTransferETH(to, amountETH); 140 | } 141 | function removeLiquidityWithPermit( 142 | address tokenA, 143 | address tokenB, 144 | uint liquidity, 145 | uint amountAMin, 146 | uint amountBMin, 147 | address to, 148 | uint deadline, 149 | bool approveMax, uint8 v, bytes32 r, bytes32 s 150 | ) external virtual override returns (uint amountA, uint amountB) { 151 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 152 | uint value = approveMax ? uint(-1) : liquidity; 153 | IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); 154 | (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); 155 | } 156 | function removeLiquidityETHWithPermit( 157 | address token, 158 | uint liquidity, 159 | uint amountTokenMin, 160 | uint amountETHMin, 161 | address to, 162 | uint deadline, 163 | bool approveMax, uint8 v, bytes32 r, bytes32 s 164 | ) external virtual override returns (uint amountToken, uint amountETH) { 165 | address pair = UniswapV2Library.pairFor(factory, token, WETH); 166 | uint value = approveMax ? uint(-1) : liquidity; 167 | IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); 168 | (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline); 169 | } 170 | 171 | // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) **** 172 | function removeLiquidityETHSupportingFeeOnTransferTokens( 173 | address token, 174 | uint liquidity, 175 | uint amountTokenMin, 176 | uint amountETHMin, 177 | address to, 178 | uint deadline 179 | ) public virtual override ensure(deadline) returns (uint amountETH) { 180 | (, amountETH) = removeLiquidity( 181 | token, 182 | WETH, 183 | liquidity, 184 | amountTokenMin, 185 | amountETHMin, 186 | address(this), 187 | deadline 188 | ); 189 | TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this))); 190 | IWETH(WETH).withdraw(amountETH); 191 | TransferHelper.safeTransferETH(to, amountETH); 192 | } 193 | function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 194 | address token, 195 | uint liquidity, 196 | uint amountTokenMin, 197 | uint amountETHMin, 198 | address to, 199 | uint deadline, 200 | bool approveMax, uint8 v, bytes32 r, bytes32 s 201 | ) external virtual override returns (uint amountETH) { 202 | address pair = UniswapV2Library.pairFor(factory, token, WETH); 203 | uint value = approveMax ? uint(-1) : liquidity; 204 | IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); 205 | amountETH = removeLiquidityETHSupportingFeeOnTransferTokens( 206 | token, liquidity, amountTokenMin, amountETHMin, to, deadline 207 | ); 208 | } 209 | 210 | // **** SWAP **** 211 | // requires the initial amount to have already been sent to the first pair 212 | function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { 213 | for (uint i; i < path.length - 1; i++) { 214 | (address input, address output) = (path[i], path[i + 1]); 215 | (address token0,) = UniswapV2Library.sortTokens(input, output); 216 | uint amountOut = amounts[i + 1]; 217 | (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); 218 | address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; 219 | IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( 220 | amount0Out, amount1Out, to, new bytes(0) 221 | ); 222 | } 223 | } 224 | function swapExactTokensForTokens( 225 | uint amountIn, 226 | uint amountOutMin, 227 | address[] calldata path, 228 | address to, 229 | uint deadline 230 | ) external virtual override ensure(deadline) returns (uint[] memory amounts) { 231 | amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); 232 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 233 | TransferHelper.safeTransferFrom( 234 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] 235 | ); 236 | _swap(amounts, path, to); 237 | } 238 | function swapTokensForExactTokens( 239 | uint amountOut, 240 | uint amountInMax, 241 | address[] calldata path, 242 | address to, 243 | uint deadline 244 | ) external virtual override ensure(deadline) returns (uint[] memory amounts) { 245 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 246 | require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 247 | TransferHelper.safeTransferFrom( 248 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] 249 | ); 250 | _swap(amounts, path, to); 251 | } 252 | function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) 253 | external 254 | virtual 255 | override 256 | payable 257 | ensure(deadline) 258 | returns (uint[] memory amounts) 259 | { 260 | require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); 261 | amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path); 262 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 263 | IWETH(WETH).deposit{value: amounts[0]}(); 264 | assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); 265 | _swap(amounts, path, to); 266 | } 267 | function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 268 | external 269 | virtual 270 | override 271 | ensure(deadline) 272 | returns (uint[] memory amounts) 273 | { 274 | require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); 275 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 276 | require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 277 | TransferHelper.safeTransferFrom( 278 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] 279 | ); 280 | _swap(amounts, path, address(this)); 281 | IWETH(WETH).withdraw(amounts[amounts.length - 1]); 282 | TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); 283 | } 284 | function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 285 | external 286 | virtual 287 | override 288 | ensure(deadline) 289 | returns (uint[] memory amounts) 290 | { 291 | require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); 292 | amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); 293 | require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 294 | TransferHelper.safeTransferFrom( 295 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] 296 | ); 297 | _swap(amounts, path, address(this)); 298 | IWETH(WETH).withdraw(amounts[amounts.length - 1]); 299 | TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]); 300 | } 301 | function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) 302 | external 303 | virtual 304 | override 305 | payable 306 | ensure(deadline) 307 | returns (uint[] memory amounts) 308 | { 309 | require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); 310 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 311 | require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); 312 | IWETH(WETH).deposit{value: amounts[0]}(); 313 | assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])); 314 | _swap(amounts, path, to); 315 | // refund dust eth, if any 316 | if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]); 317 | } 318 | 319 | // **** SWAP (supporting fee-on-transfer tokens) **** 320 | // requires the initial amount to have already been sent to the first pair 321 | function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual { 322 | for (uint i; i < path.length - 1; i++) { 323 | (address input, address output) = (path[i], path[i + 1]); 324 | (address token0,) = UniswapV2Library.sortTokens(input, output); 325 | IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)); 326 | uint amountInput; 327 | uint amountOutput; 328 | { // scope to avoid stack too deep errors 329 | (uint reserve0, uint reserve1,) = pair.getReserves(); 330 | (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 331 | amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput); 332 | amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); 333 | } 334 | (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0)); 335 | address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; 336 | pair.swap(amount0Out, amount1Out, to, new bytes(0)); 337 | } 338 | } 339 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 340 | uint amountIn, 341 | uint amountOutMin, 342 | address[] calldata path, 343 | address to, 344 | uint deadline 345 | ) external virtual override ensure(deadline) { 346 | TransferHelper.safeTransferFrom( 347 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn 348 | ); 349 | uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); 350 | _swapSupportingFeeOnTransferTokens(path, to); 351 | require( 352 | IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, 353 | 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' 354 | ); 355 | } 356 | function swapExactETHForTokensSupportingFeeOnTransferTokens( 357 | uint amountOutMin, 358 | address[] calldata path, 359 | address to, 360 | uint deadline 361 | ) 362 | external 363 | virtual 364 | override 365 | payable 366 | ensure(deadline) 367 | { 368 | require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH'); 369 | uint amountIn = msg.value; 370 | IWETH(WETH).deposit{value: amountIn}(); 371 | assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn)); 372 | uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); 373 | _swapSupportingFeeOnTransferTokens(path, to); 374 | require( 375 | IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin, 376 | 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT' 377 | ); 378 | } 379 | function swapExactTokensForETHSupportingFeeOnTransferTokens( 380 | uint amountIn, 381 | uint amountOutMin, 382 | address[] calldata path, 383 | address to, 384 | uint deadline 385 | ) 386 | external 387 | virtual 388 | override 389 | ensure(deadline) 390 | { 391 | require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH'); 392 | TransferHelper.safeTransferFrom( 393 | path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn 394 | ); 395 | _swapSupportingFeeOnTransferTokens(path, address(this)); 396 | uint amountOut = IERC20(WETH).balanceOf(address(this)); 397 | require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); 398 | IWETH(WETH).withdraw(amountOut); 399 | TransferHelper.safeTransferETH(to, amountOut); 400 | } 401 | 402 | // **** LIBRARY FUNCTIONS **** 403 | function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) { 404 | return UniswapV2Library.quote(amountA, reserveA, reserveB); 405 | } 406 | 407 | function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) 408 | public 409 | pure 410 | virtual 411 | override 412 | returns (uint amountOut) 413 | { 414 | return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); 415 | } 416 | 417 | function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) 418 | public 419 | pure 420 | virtual 421 | override 422 | returns (uint amountIn) 423 | { 424 | return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut); 425 | } 426 | 427 | function getAmountsOut(uint amountIn, address[] memory path) 428 | public 429 | view 430 | virtual 431 | override 432 | returns (uint[] memory amounts) 433 | { 434 | return UniswapV2Library.getAmountsOut(factory, amountIn, path); 435 | } 436 | 437 | function getAmountsIn(uint amountOut, address[] memory path) 438 | public 439 | view 440 | virtual 441 | override 442 | returns (uint[] memory amounts) 443 | { 444 | return UniswapV2Library.getAmountsIn(factory, amountOut, path); 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /contracts/examples/ExampleComputeLiquidityValue.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '../libraries/UniswapV2LiquidityMathLibrary.sol'; 4 | 5 | contract ExampleComputeLiquidityValue { 6 | using SafeMath for uint256; 7 | 8 | address public immutable factory; 9 | 10 | constructor(address factory_) public { 11 | factory = factory_; 12 | } 13 | 14 | // see UniswapV2LiquidityMathLibrary#getReservesAfterArbitrage 15 | function getReservesAfterArbitrage( 16 | address tokenA, 17 | address tokenB, 18 | uint256 truePriceTokenA, 19 | uint256 truePriceTokenB 20 | ) external view returns (uint256 reserveA, uint256 reserveB) { 21 | return UniswapV2LiquidityMathLibrary.getReservesAfterArbitrage( 22 | factory, 23 | tokenA, 24 | tokenB, 25 | truePriceTokenA, 26 | truePriceTokenB 27 | ); 28 | } 29 | 30 | // see UniswapV2LiquidityMathLibrary#getLiquidityValue 31 | function getLiquidityValue( 32 | address tokenA, 33 | address tokenB, 34 | uint256 liquidityAmount 35 | ) external view returns ( 36 | uint256 tokenAAmount, 37 | uint256 tokenBAmount 38 | ) { 39 | return UniswapV2LiquidityMathLibrary.getLiquidityValue( 40 | factory, 41 | tokenA, 42 | tokenB, 43 | liquidityAmount 44 | ); 45 | } 46 | 47 | // see UniswapV2LiquidityMathLibrary#getLiquidityValueAfterArbitrageToPrice 48 | function getLiquidityValueAfterArbitrageToPrice( 49 | address tokenA, 50 | address tokenB, 51 | uint256 truePriceTokenA, 52 | uint256 truePriceTokenB, 53 | uint256 liquidityAmount 54 | ) external view returns ( 55 | uint256 tokenAAmount, 56 | uint256 tokenBAmount 57 | ) { 58 | return UniswapV2LiquidityMathLibrary.getLiquidityValueAfterArbitrageToPrice( 59 | factory, 60 | tokenA, 61 | tokenB, 62 | truePriceTokenA, 63 | truePriceTokenB, 64 | liquidityAmount 65 | ); 66 | } 67 | 68 | // test function to measure the gas cost of the above function 69 | function getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 70 | address tokenA, 71 | address tokenB, 72 | uint256 truePriceTokenA, 73 | uint256 truePriceTokenB, 74 | uint256 liquidityAmount 75 | ) external view returns ( 76 | uint256 77 | ) { 78 | uint gasBefore = gasleft(); 79 | UniswapV2LiquidityMathLibrary.getLiquidityValueAfterArbitrageToPrice( 80 | factory, 81 | tokenA, 82 | tokenB, 83 | truePriceTokenA, 84 | truePriceTokenB, 85 | liquidityAmount 86 | ); 87 | uint gasAfter = gasleft(); 88 | return gasBefore - gasAfter; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/examples/ExampleFlashSwap.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol'; 4 | 5 | import '../libraries/UniswapV2Library.sol'; 6 | import '../interfaces/V1/IUniswapV1Factory.sol'; 7 | import '../interfaces/V1/IUniswapV1Exchange.sol'; 8 | import '../interfaces/IUniswapV2Router01.sol'; 9 | import '../interfaces/IERC20.sol'; 10 | import '../interfaces/IWETH.sol'; 11 | 12 | contract ExampleFlashSwap is IUniswapV2Callee { 13 | IUniswapV1Factory immutable factoryV1; 14 | address immutable factory; 15 | IWETH immutable WETH; 16 | 17 | constructor(address _factory, address _factoryV1, address router) public { 18 | factoryV1 = IUniswapV1Factory(_factoryV1); 19 | factory = _factory; 20 | WETH = IWETH(IUniswapV2Router01(router).WETH()); 21 | } 22 | 23 | // needs to accept ETH from any V1 exchange and WETH. ideally this could be enforced, as in the router, 24 | // but it's not possible because it requires a call to the v1 factory, which takes too much gas 25 | receive() external payable {} 26 | 27 | // gets tokens/WETH via a V2 flash swap, swaps for the ETH/tokens on V1, repays V2, and keeps the rest! 28 | function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external override { 29 | address[] memory path = new address[](2); 30 | uint amountToken; 31 | uint amountETH; 32 | { // scope for token{0,1}, avoids stack too deep errors 33 | address token0 = IUniswapV2Pair(msg.sender).token0(); 34 | address token1 = IUniswapV2Pair(msg.sender).token1(); 35 | assert(msg.sender == UniswapV2Library.pairFor(factory, token0, token1)); // ensure that msg.sender is actually a V2 pair 36 | assert(amount0 == 0 || amount1 == 0); // this strategy is unidirectional 37 | path[0] = amount0 == 0 ? token0 : token1; 38 | path[1] = amount0 == 0 ? token1 : token0; 39 | amountToken = token0 == address(WETH) ? amount1 : amount0; 40 | amountETH = token0 == address(WETH) ? amount0 : amount1; 41 | } 42 | 43 | assert(path[0] == address(WETH) || path[1] == address(WETH)); // this strategy only works with a V2 WETH pair 44 | IERC20 token = IERC20(path[0] == address(WETH) ? path[1] : path[0]); 45 | IUniswapV1Exchange exchangeV1 = IUniswapV1Exchange(factoryV1.getExchange(address(token))); // get V1 exchange 46 | 47 | if (amountToken > 0) { 48 | (uint minETH) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller 49 | token.approve(address(exchangeV1), amountToken); 50 | uint amountReceived = exchangeV1.tokenToEthSwapInput(amountToken, minETH, uint(-1)); 51 | uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountToken, path)[0]; 52 | assert(amountReceived > amountRequired); // fail if we didn't get enough ETH back to repay our flash loan 53 | WETH.deposit{value: amountRequired}(); 54 | assert(WETH.transfer(msg.sender, amountRequired)); // return WETH to V2 pair 55 | (bool success,) = sender.call{value: amountReceived - amountRequired}(new bytes(0)); // keep the rest! (ETH) 56 | assert(success); 57 | } else { 58 | (uint minTokens) = abi.decode(data, (uint)); // slippage parameter for V1, passed in by caller 59 | WETH.withdraw(amountETH); 60 | uint amountReceived = exchangeV1.ethToTokenSwapInput{value: amountETH}(minTokens, uint(-1)); 61 | uint amountRequired = UniswapV2Library.getAmountsIn(factory, amountETH, path)[0]; 62 | assert(amountReceived > amountRequired); // fail if we didn't get enough tokens back to repay our flash loan 63 | assert(token.transfer(msg.sender, amountRequired)); // return tokens to V2 pair 64 | assert(token.transfer(sender, amountReceived - amountRequired)); // keep the rest! (tokens) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/examples/ExampleOracleSimple.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; 4 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 5 | import '@uniswap/lib/contracts/libraries/FixedPoint.sol'; 6 | 7 | import '../libraries/UniswapV2OracleLibrary.sol'; 8 | import '../libraries/UniswapV2Library.sol'; 9 | 10 | // fixed window oracle that recomputes the average price for the entire period once every period 11 | // note that the price average is only guaranteed to be over at least 1 period, but may be over a longer period 12 | contract ExampleOracleSimple { 13 | using FixedPoint for *; 14 | 15 | uint public constant PERIOD = 24 hours; 16 | 17 | IUniswapV2Pair immutable pair; 18 | address public immutable token0; 19 | address public immutable token1; 20 | 21 | uint public price0CumulativeLast; 22 | uint public price1CumulativeLast; 23 | uint32 public blockTimestampLast; 24 | FixedPoint.uq112x112 public price0Average; 25 | FixedPoint.uq112x112 public price1Average; 26 | 27 | constructor(address factory, address tokenA, address tokenB) public { 28 | IUniswapV2Pair _pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, tokenA, tokenB)); 29 | pair = _pair; 30 | token0 = _pair.token0(); 31 | token1 = _pair.token1(); 32 | price0CumulativeLast = _pair.price0CumulativeLast(); // fetch the current accumulated price value (1 / 0) 33 | price1CumulativeLast = _pair.price1CumulativeLast(); // fetch the current accumulated price value (0 / 1) 34 | uint112 reserve0; 35 | uint112 reserve1; 36 | (reserve0, reserve1, blockTimestampLast) = _pair.getReserves(); 37 | require(reserve0 != 0 && reserve1 != 0, 'ExampleOracleSimple: NO_RESERVES'); // ensure that there's liquidity in the pair 38 | } 39 | 40 | function update() external { 41 | (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) = 42 | UniswapV2OracleLibrary.currentCumulativePrices(address(pair)); 43 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired 44 | 45 | // ensure that at least one full period has passed since the last update 46 | require(timeElapsed >= PERIOD, 'ExampleOracleSimple: PERIOD_NOT_ELAPSED'); 47 | 48 | // overflow is desired, casting never truncates 49 | // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed 50 | price0Average = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed)); 51 | price1Average = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed)); 52 | 53 | price0CumulativeLast = price0Cumulative; 54 | price1CumulativeLast = price1Cumulative; 55 | blockTimestampLast = blockTimestamp; 56 | } 57 | 58 | // note this will always return 0 before update has been called successfully for the first time. 59 | function consult(address token, uint amountIn) external view returns (uint amountOut) { 60 | if (token == token0) { 61 | amountOut = price0Average.mul(amountIn).decode144(); 62 | } else { 63 | require(token == token1, 'ExampleOracleSimple: INVALID_TOKEN'); 64 | amountOut = price1Average.mul(amountIn).decode144(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/examples/ExampleSlidingWindowOracle.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; 4 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 5 | import '@uniswap/lib/contracts/libraries/FixedPoint.sol'; 6 | 7 | import '../libraries/SafeMath.sol'; 8 | import '../libraries/UniswapV2Library.sol'; 9 | import '../libraries/UniswapV2OracleLibrary.sol'; 10 | 11 | // sliding window oracle that uses observations collected over a window to provide moving price averages in the past 12 | // `windowSize` with a precision of `windowSize / granularity` 13 | // note this is a singleton oracle and only needs to be deployed once per desired parameters, which 14 | // differs from the simple oracle which must be deployed once per pair. 15 | contract ExampleSlidingWindowOracle { 16 | using FixedPoint for *; 17 | using SafeMath for uint; 18 | 19 | struct Observation { 20 | uint timestamp; 21 | uint price0Cumulative; 22 | uint price1Cumulative; 23 | } 24 | 25 | address public immutable factory; 26 | // the desired amount of time over which the moving average should be computed, e.g. 24 hours 27 | uint public immutable windowSize; 28 | // the number of observations stored for each pair, i.e. how many price observations are stored for the window. 29 | // as granularity increases from 1, more frequent updates are needed, but moving averages become more precise. 30 | // averages are computed over intervals with sizes in the range: 31 | // [windowSize - (windowSize / granularity) * 2, windowSize] 32 | // e.g. if the window size is 24 hours, and the granularity is 24, the oracle will return the average price for 33 | // the period: 34 | // [now - [22 hours, 24 hours], now] 35 | uint8 public immutable granularity; 36 | // this is redundant with granularity and windowSize, but stored for gas savings & informational purposes. 37 | uint public immutable periodSize; 38 | 39 | // mapping from pair address to a list of price observations of that pair 40 | mapping(address => Observation[]) public pairObservations; 41 | 42 | constructor(address factory_, uint windowSize_, uint8 granularity_) public { 43 | require(granularity_ > 1, 'SlidingWindowOracle: GRANULARITY'); 44 | require( 45 | (periodSize = windowSize_ / granularity_) * granularity_ == windowSize_, 46 | 'SlidingWindowOracle: WINDOW_NOT_EVENLY_DIVISIBLE' 47 | ); 48 | factory = factory_; 49 | windowSize = windowSize_; 50 | granularity = granularity_; 51 | } 52 | 53 | // returns the index of the observation corresponding to the given timestamp 54 | function observationIndexOf(uint timestamp) public view returns (uint8 index) { 55 | uint epochPeriod = timestamp / periodSize; 56 | return uint8(epochPeriod % granularity); 57 | } 58 | 59 | // returns the observation from the oldest epoch (at the beginning of the window) relative to the current time 60 | function getFirstObservationInWindow(address pair) private view returns (Observation storage firstObservation) { 61 | uint8 observationIndex = observationIndexOf(block.timestamp); 62 | // no overflow issue. if observationIndex + 1 overflows, result is still zero. 63 | uint8 firstObservationIndex = (observationIndex + 1) % granularity; 64 | firstObservation = pairObservations[pair][firstObservationIndex]; 65 | } 66 | 67 | // update the cumulative price for the observation at the current timestamp. each observation is updated at most 68 | // once per epoch period. 69 | function update(address tokenA, address tokenB) external { 70 | address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); 71 | 72 | // populate the array with empty observations (first call only) 73 | for (uint i = pairObservations[pair].length; i < granularity; i++) { 74 | pairObservations[pair].push(); 75 | } 76 | 77 | // get the observation for the current period 78 | uint8 observationIndex = observationIndexOf(block.timestamp); 79 | Observation storage observation = pairObservations[pair][observationIndex]; 80 | 81 | // we only want to commit updates once per period (i.e. windowSize / granularity) 82 | uint timeElapsed = block.timestamp - observation.timestamp; 83 | if (timeElapsed > periodSize) { 84 | (uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair); 85 | observation.timestamp = block.timestamp; 86 | observation.price0Cumulative = price0Cumulative; 87 | observation.price1Cumulative = price1Cumulative; 88 | } 89 | } 90 | 91 | // given the cumulative prices of the start and end of a period, and the length of the period, compute the average 92 | // price in terms of how much amount out is received for the amount in 93 | function computeAmountOut( 94 | uint priceCumulativeStart, uint priceCumulativeEnd, 95 | uint timeElapsed, uint amountIn 96 | ) private pure returns (uint amountOut) { 97 | // overflow is desired. 98 | FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112( 99 | uint224((priceCumulativeEnd - priceCumulativeStart) / timeElapsed) 100 | ); 101 | amountOut = priceAverage.mul(amountIn).decode144(); 102 | } 103 | 104 | // returns the amount out corresponding to the amount in for a given token using the moving average over the time 105 | // range [now - [windowSize, windowSize - periodSize * 2], now] 106 | // update must have been called for the bucket corresponding to timestamp `now - windowSize` 107 | function consult(address tokenIn, uint amountIn, address tokenOut) external view returns (uint amountOut) { 108 | address pair = UniswapV2Library.pairFor(factory, tokenIn, tokenOut); 109 | Observation storage firstObservation = getFirstObservationInWindow(pair); 110 | 111 | uint timeElapsed = block.timestamp - firstObservation.timestamp; 112 | require(timeElapsed <= windowSize, 'SlidingWindowOracle: MISSING_HISTORICAL_OBSERVATION'); 113 | // should never happen. 114 | require(timeElapsed >= windowSize - periodSize * 2, 'SlidingWindowOracle: UNEXPECTED_TIME_ELAPSED'); 115 | 116 | (uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair); 117 | (address token0,) = UniswapV2Library.sortTokens(tokenIn, tokenOut); 118 | 119 | if (token0 == tokenIn) { 120 | return computeAmountOut(firstObservation.price0Cumulative, price0Cumulative, timeElapsed, amountIn); 121 | } else { 122 | return computeAmountOut(firstObservation.price1Cumulative, price1Cumulative, timeElapsed, amountIn); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /contracts/examples/ExampleSwapToPrice.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 4 | import '@uniswap/lib/contracts/libraries/Babylonian.sol'; 5 | import '@uniswap/lib/contracts/libraries/TransferHelper.sol'; 6 | 7 | import '../libraries/UniswapV2LiquidityMathLibrary.sol'; 8 | import '../interfaces/IERC20.sol'; 9 | import '../interfaces/IUniswapV2Router01.sol'; 10 | import '../libraries/SafeMath.sol'; 11 | import '../libraries/UniswapV2Library.sol'; 12 | 13 | contract ExampleSwapToPrice { 14 | using SafeMath for uint256; 15 | 16 | IUniswapV2Router01 public immutable router; 17 | address public immutable factory; 18 | 19 | constructor(address factory_, IUniswapV2Router01 router_) public { 20 | factory = factory_; 21 | router = router_; 22 | } 23 | 24 | // swaps an amount of either token such that the trade is profit-maximizing, given an external true price 25 | // true price is expressed in the ratio of token A to token B 26 | // caller must approve this contract to spend whichever token is intended to be swapped 27 | function swapToPrice( 28 | address tokenA, 29 | address tokenB, 30 | uint256 truePriceTokenA, 31 | uint256 truePriceTokenB, 32 | uint256 maxSpendTokenA, 33 | uint256 maxSpendTokenB, 34 | address to, 35 | uint256 deadline 36 | ) public { 37 | // true price is expressed as a ratio, so both values must be non-zero 38 | require(truePriceTokenA != 0 && truePriceTokenB != 0, "ExampleSwapToPrice: ZERO_PRICE"); 39 | // caller can specify 0 for either if they wish to swap in only one direction, but not both 40 | require(maxSpendTokenA != 0 || maxSpendTokenB != 0, "ExampleSwapToPrice: ZERO_SPEND"); 41 | 42 | bool aToB; 43 | uint256 amountIn; 44 | { 45 | (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 46 | (aToB, amountIn) = UniswapV2LiquidityMathLibrary.computeProfitMaximizingTrade( 47 | truePriceTokenA, truePriceTokenB, 48 | reserveA, reserveB 49 | ); 50 | } 51 | 52 | require(amountIn > 0, 'ExampleSwapToPrice: ZERO_AMOUNT_IN'); 53 | 54 | // spend up to the allowance of the token in 55 | uint256 maxSpend = aToB ? maxSpendTokenA : maxSpendTokenB; 56 | if (amountIn > maxSpend) { 57 | amountIn = maxSpend; 58 | } 59 | 60 | address tokenIn = aToB ? tokenA : tokenB; 61 | address tokenOut = aToB ? tokenB : tokenA; 62 | TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amountIn); 63 | TransferHelper.safeApprove(tokenIn, address(router), amountIn); 64 | 65 | address[] memory path = new address[](2); 66 | path[0] = tokenIn; 67 | path[1] = tokenOut; 68 | 69 | router.swapExactTokensForTokens( 70 | amountIn, 71 | 0, // amountOutMin: we can skip computing this number because the math is tested 72 | path, 73 | to, 74 | deadline 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Disclaimer 4 | 5 | These contracts are for demonstrative purposes only. 6 | While these contracts have unit tests, and we generally expect them to be 7 | correct, there are no guarantees about the correctness or security of 8 | these contracts. We hold these contracts to a different standard of 9 | correctness and security than other contracts in this repository. 10 | E.g., we have explicitly excluded these contracts from the 11 | [bug bounty](https://uniswap.org/bug-bounty/#scope). 12 | 13 | You must do your own due diligence if you wish to use code 14 | from these examples in your project. 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IERC20 { 4 | event Approval(address indexed owner, address indexed spender, uint value); 5 | event Transfer(address indexed from, address indexed to, uint value); 6 | 7 | function name() external view returns (string memory); 8 | function symbol() external view returns (string memory); 9 | function decimals() external view returns (uint8); 10 | function totalSupply() external view returns (uint); 11 | function balanceOf(address owner) external view returns (uint); 12 | function allowance(address owner, address spender) external view returns (uint); 13 | 14 | function approve(address spender, uint value) external returns (bool); 15 | function transfer(address to, uint value) external returns (bool); 16 | function transferFrom(address from, address to, uint value) external returns (bool); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Migrator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IUniswapV2Migrator { 4 | function migrate(address token, uint amountTokenMin, uint amountETHMin, address to, uint deadline) external; 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router01.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.2; 2 | 3 | interface IUniswapV2Router01 { 4 | function factory() external pure returns (address); 5 | function WETH() external pure returns (address); 6 | 7 | function addLiquidity( 8 | address tokenA, 9 | address tokenB, 10 | uint amountADesired, 11 | uint amountBDesired, 12 | uint amountAMin, 13 | uint amountBMin, 14 | address to, 15 | uint deadline 16 | ) external returns (uint amountA, uint amountB, uint liquidity); 17 | function addLiquidityETH( 18 | address token, 19 | uint amountTokenDesired, 20 | uint amountTokenMin, 21 | uint amountETHMin, 22 | address to, 23 | uint deadline 24 | ) external payable returns (uint amountToken, uint amountETH, uint liquidity); 25 | function removeLiquidity( 26 | address tokenA, 27 | address tokenB, 28 | uint liquidity, 29 | uint amountAMin, 30 | uint amountBMin, 31 | address to, 32 | uint deadline 33 | ) external returns (uint amountA, uint amountB); 34 | function removeLiquidityETH( 35 | address token, 36 | uint liquidity, 37 | uint amountTokenMin, 38 | uint amountETHMin, 39 | address to, 40 | uint deadline 41 | ) external returns (uint amountToken, uint amountETH); 42 | function removeLiquidityWithPermit( 43 | address tokenA, 44 | address tokenB, 45 | uint liquidity, 46 | uint amountAMin, 47 | uint amountBMin, 48 | address to, 49 | uint deadline, 50 | bool approveMax, uint8 v, bytes32 r, bytes32 s 51 | ) external returns (uint amountA, uint amountB); 52 | function removeLiquidityETHWithPermit( 53 | address token, 54 | uint liquidity, 55 | uint amountTokenMin, 56 | uint amountETHMin, 57 | address to, 58 | uint deadline, 59 | bool approveMax, uint8 v, bytes32 r, bytes32 s 60 | ) external returns (uint amountToken, uint amountETH); 61 | function swapExactTokensForTokens( 62 | uint amountIn, 63 | uint amountOutMin, 64 | address[] calldata path, 65 | address to, 66 | uint deadline 67 | ) external returns (uint[] memory amounts); 68 | function swapTokensForExactTokens( 69 | uint amountOut, 70 | uint amountInMax, 71 | address[] calldata path, 72 | address to, 73 | uint deadline 74 | ) external returns (uint[] memory amounts); 75 | function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) 76 | external 77 | payable 78 | returns (uint[] memory amounts); 79 | function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 80 | external 81 | returns (uint[] memory amounts); 82 | function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 83 | external 84 | returns (uint[] memory amounts); 85 | function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) 86 | external 87 | payable 88 | returns (uint[] memory amounts); 89 | 90 | function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); 91 | function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); 92 | function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); 93 | function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); 94 | function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); 95 | } 96 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.2; 2 | 3 | import './IUniswapV2Router01.sol'; 4 | 5 | interface IUniswapV2Router02 is IUniswapV2Router01 { 6 | function removeLiquidityETHSupportingFeeOnTransferTokens( 7 | address token, 8 | uint liquidity, 9 | uint amountTokenMin, 10 | uint amountETHMin, 11 | address to, 12 | uint deadline 13 | ) external returns (uint amountETH); 14 | function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 15 | address token, 16 | uint liquidity, 17 | uint amountTokenMin, 18 | uint amountETHMin, 19 | address to, 20 | uint deadline, 21 | bool approveMax, uint8 v, bytes32 r, bytes32 s 22 | ) external returns (uint amountETH); 23 | 24 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 25 | uint amountIn, 26 | uint amountOutMin, 27 | address[] calldata path, 28 | address to, 29 | uint deadline 30 | ) external; 31 | function swapExactETHForTokensSupportingFeeOnTransferTokens( 32 | uint amountOutMin, 33 | address[] calldata path, 34 | address to, 35 | uint deadline 36 | ) external payable; 37 | function swapExactTokensForETHSupportingFeeOnTransferTokens( 38 | uint amountIn, 39 | uint amountOutMin, 40 | address[] calldata path, 41 | address to, 42 | uint deadline 43 | ) external; 44 | } 45 | -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IWETH { 4 | function deposit() external payable; 5 | function transfer(address to, uint value) external returns (bool); 6 | function withdraw(uint) external; 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/V1/IUniswapV1Exchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IUniswapV1Exchange { 4 | function balanceOf(address owner) external view returns (uint); 5 | function transferFrom(address from, address to, uint value) external returns (bool); 6 | function removeLiquidity(uint, uint, uint, uint) external returns (uint, uint); 7 | function tokenToEthSwapInput(uint, uint, uint) external returns (uint); 8 | function ethToTokenSwapInput(uint, uint) external payable returns (uint); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/V1/IUniswapV1Factory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IUniswapV1Factory { 4 | function getExchange(address) external view returns (address); 5 | } 6 | -------------------------------------------------------------------------------- /contracts/libraries/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | // a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) 4 | 5 | library SafeMath { 6 | function add(uint x, uint y) internal pure returns (uint z) { 7 | require((z = x + y) >= x, 'ds-math-add-overflow'); 8 | } 9 | 10 | function sub(uint x, uint y) internal pure returns (uint z) { 11 | require((z = x - y) <= x, 'ds-math-sub-underflow'); 12 | } 13 | 14 | function mul(uint x, uint y) internal pure returns (uint z) { 15 | require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/libraries/UniswapV2Library.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 4 | 5 | import "./SafeMath.sol"; 6 | 7 | library UniswapV2Library { 8 | using SafeMath for uint; 9 | 10 | // returns sorted token addresses, used to handle return values from pairs sorted in this order 11 | function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { 12 | require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); 13 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 14 | require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); 15 | } 16 | 17 | // calculates the CREATE2 address for a pair without making any external calls 18 | function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { 19 | (address token0, address token1) = sortTokens(tokenA, tokenB); 20 | pair = address(uint(keccak256(abi.encodePacked( 21 | hex'ff', 22 | factory, 23 | keccak256(abi.encodePacked(token0, token1)), 24 | hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash 25 | )))); 26 | } 27 | 28 | // fetches and sorts the reserves for a pair 29 | function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { 30 | (address token0,) = sortTokens(tokenA, tokenB); 31 | (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); 32 | (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 33 | } 34 | 35 | // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset 36 | function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { 37 | require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); 38 | require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); 39 | amountB = amountA.mul(reserveB) / reserveA; 40 | } 41 | 42 | // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset 43 | function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { 44 | require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); 45 | require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); 46 | uint amountInWithFee = amountIn.mul(997); 47 | uint numerator = amountInWithFee.mul(reserveOut); 48 | uint denominator = reserveIn.mul(1000).add(amountInWithFee); 49 | amountOut = numerator / denominator; 50 | } 51 | 52 | // given an output amount of an asset and pair reserves, returns a required input amount of the other asset 53 | function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { 54 | require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); 55 | require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); 56 | uint numerator = reserveIn.mul(amountOut).mul(1000); 57 | uint denominator = reserveOut.sub(amountOut).mul(997); 58 | amountIn = (numerator / denominator).add(1); 59 | } 60 | 61 | // performs chained getAmountOut calculations on any number of pairs 62 | function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { 63 | require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); 64 | amounts = new uint[](path.length); 65 | amounts[0] = amountIn; 66 | for (uint i; i < path.length - 1; i++) { 67 | (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); 68 | amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); 69 | } 70 | } 71 | 72 | // performs chained getAmountIn calculations on any number of pairs 73 | function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { 74 | require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); 75 | amounts = new uint[](path.length); 76 | amounts[amounts.length - 1] = amountOut; 77 | for (uint i = path.length - 1; i > 0; i--) { 78 | (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); 79 | amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/libraries/UniswapV2LiquidityMathLibrary.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 4 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol'; 5 | import '@uniswap/lib/contracts/libraries/Babylonian.sol'; 6 | import '@uniswap/lib/contracts/libraries/FullMath.sol'; 7 | 8 | import './SafeMath.sol'; 9 | import './UniswapV2Library.sol'; 10 | 11 | // library containing some math for dealing with the liquidity shares of a pair, e.g. computing their exact value 12 | // in terms of the underlying tokens 13 | library UniswapV2LiquidityMathLibrary { 14 | using SafeMath for uint256; 15 | 16 | // computes the direction and magnitude of the profit-maximizing trade 17 | function computeProfitMaximizingTrade( 18 | uint256 truePriceTokenA, 19 | uint256 truePriceTokenB, 20 | uint256 reserveA, 21 | uint256 reserveB 22 | ) pure internal returns (bool aToB, uint256 amountIn) { 23 | aToB = FullMath.mulDiv(reserveA, truePriceTokenB, reserveB) < truePriceTokenA; 24 | 25 | uint256 invariant = reserveA.mul(reserveB); 26 | 27 | uint256 leftSide = Babylonian.sqrt( 28 | FullMath.mulDiv( 29 | invariant.mul(1000), 30 | aToB ? truePriceTokenA : truePriceTokenB, 31 | (aToB ? truePriceTokenB : truePriceTokenA).mul(997) 32 | ) 33 | ); 34 | uint256 rightSide = (aToB ? reserveA.mul(1000) : reserveB.mul(1000)) / 997; 35 | 36 | if (leftSide < rightSide) return (false, 0); 37 | 38 | // compute the amount that must be sent to move the price to the profit-maximizing price 39 | amountIn = leftSide.sub(rightSide); 40 | } 41 | 42 | // gets the reserves after an arbitrage moves the price to the profit-maximizing ratio given an externally observed true price 43 | function getReservesAfterArbitrage( 44 | address factory, 45 | address tokenA, 46 | address tokenB, 47 | uint256 truePriceTokenA, 48 | uint256 truePriceTokenB 49 | ) view internal returns (uint256 reserveA, uint256 reserveB) { 50 | // first get reserves before the swap 51 | (reserveA, reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 52 | 53 | require(reserveA > 0 && reserveB > 0, 'UniswapV2ArbitrageLibrary: ZERO_PAIR_RESERVES'); 54 | 55 | // then compute how much to swap to arb to the true price 56 | (bool aToB, uint256 amountIn) = computeProfitMaximizingTrade(truePriceTokenA, truePriceTokenB, reserveA, reserveB); 57 | 58 | if (amountIn == 0) { 59 | return (reserveA, reserveB); 60 | } 61 | 62 | // now affect the trade to the reserves 63 | if (aToB) { 64 | uint amountOut = UniswapV2Library.getAmountOut(amountIn, reserveA, reserveB); 65 | reserveA += amountIn; 66 | reserveB -= amountOut; 67 | } else { 68 | uint amountOut = UniswapV2Library.getAmountOut(amountIn, reserveB, reserveA); 69 | reserveB += amountIn; 70 | reserveA -= amountOut; 71 | } 72 | } 73 | 74 | // computes liquidity value given all the parameters of the pair 75 | function computeLiquidityValue( 76 | uint256 reservesA, 77 | uint256 reservesB, 78 | uint256 totalSupply, 79 | uint256 liquidityAmount, 80 | bool feeOn, 81 | uint kLast 82 | ) internal pure returns (uint256 tokenAAmount, uint256 tokenBAmount) { 83 | if (feeOn && kLast > 0) { 84 | uint rootK = Babylonian.sqrt(reservesA.mul(reservesB)); 85 | uint rootKLast = Babylonian.sqrt(kLast); 86 | if (rootK > rootKLast) { 87 | uint numerator1 = totalSupply; 88 | uint numerator2 = rootK.sub(rootKLast); 89 | uint denominator = rootK.mul(5).add(rootKLast); 90 | uint feeLiquidity = FullMath.mulDiv(numerator1, numerator2, denominator); 91 | totalSupply = totalSupply.add(feeLiquidity); 92 | } 93 | } 94 | return (reservesA.mul(liquidityAmount) / totalSupply, reservesB.mul(liquidityAmount) / totalSupply); 95 | } 96 | 97 | // get all current parameters from the pair and compute value of a liquidity amount 98 | // **note this is subject to manipulation, e.g. sandwich attacks**. prefer passing a manipulation resistant price to 99 | // #getLiquidityValueAfterArbitrageToPrice 100 | function getLiquidityValue( 101 | address factory, 102 | address tokenA, 103 | address tokenB, 104 | uint256 liquidityAmount 105 | ) internal view returns (uint256 tokenAAmount, uint256 tokenBAmount) { 106 | (uint256 reservesA, uint256 reservesB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); 107 | IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, tokenA, tokenB)); 108 | bool feeOn = IUniswapV2Factory(factory).feeTo() != address(0); 109 | uint kLast = feeOn ? pair.kLast() : 0; 110 | uint totalSupply = pair.totalSupply(); 111 | return computeLiquidityValue(reservesA, reservesB, totalSupply, liquidityAmount, feeOn, kLast); 112 | } 113 | 114 | // given two tokens, tokenA and tokenB, and their "true price", i.e. the observed ratio of value of token A to token B, 115 | // and a liquidity amount, returns the value of the liquidity in terms of tokenA and tokenB 116 | function getLiquidityValueAfterArbitrageToPrice( 117 | address factory, 118 | address tokenA, 119 | address tokenB, 120 | uint256 truePriceTokenA, 121 | uint256 truePriceTokenB, 122 | uint256 liquidityAmount 123 | ) internal view returns ( 124 | uint256 tokenAAmount, 125 | uint256 tokenBAmount 126 | ) { 127 | bool feeOn = IUniswapV2Factory(factory).feeTo() != address(0); 128 | IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, tokenA, tokenB)); 129 | uint kLast = feeOn ? pair.kLast() : 0; 130 | uint totalSupply = pair.totalSupply(); 131 | 132 | // this also checks that totalSupply > 0 133 | require(totalSupply >= liquidityAmount && liquidityAmount > 0, 'ComputeLiquidityValue: LIQUIDITY_AMOUNT'); 134 | 135 | (uint reservesA, uint reservesB) = getReservesAfterArbitrage(factory, tokenA, tokenB, truePriceTokenA, truePriceTokenB); 136 | 137 | return computeLiquidityValue(reservesA, reservesB, totalSupply, liquidityAmount, feeOn, kLast); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /contracts/libraries/UniswapV2OracleLibrary.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 4 | import '@uniswap/lib/contracts/libraries/FixedPoint.sol'; 5 | 6 | // library with helper methods for oracles that are concerned with computing average prices 7 | library UniswapV2OracleLibrary { 8 | using FixedPoint for *; 9 | 10 | // helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] 11 | function currentBlockTimestamp() internal view returns (uint32) { 12 | return uint32(block.timestamp % 2 ** 32); 13 | } 14 | 15 | // produces the cumulative price using counterfactuals to save gas and avoid a call to sync. 16 | function currentCumulativePrices( 17 | address pair 18 | ) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { 19 | blockTimestamp = currentBlockTimestamp(); 20 | price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); 21 | price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); 22 | 23 | // if time has elapsed since the last update on the pair, mock the accumulated price values 24 | (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); 25 | if (blockTimestampLast != blockTimestamp) { 26 | // subtraction overflow is desired 27 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 28 | // addition overflow is desired 29 | // counterfactual 30 | price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; 31 | // counterfactual 32 | price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /contracts/test/DeflatingERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '../libraries/SafeMath.sol'; 4 | 5 | contract DeflatingERC20 { 6 | using SafeMath for uint; 7 | 8 | string public constant name = 'Deflating Test Token'; 9 | string public constant symbol = 'DTT'; 10 | uint8 public constant decimals = 18; 11 | uint public totalSupply; 12 | mapping(address => uint) public balanceOf; 13 | mapping(address => mapping(address => uint)) public allowance; 14 | 15 | bytes32 public DOMAIN_SEPARATOR; 16 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 17 | bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 18 | mapping(address => uint) public nonces; 19 | 20 | event Approval(address indexed owner, address indexed spender, uint value); 21 | event Transfer(address indexed from, address indexed to, uint value); 22 | 23 | constructor(uint _totalSupply) public { 24 | uint chainId; 25 | assembly { 26 | chainId := chainid() 27 | } 28 | DOMAIN_SEPARATOR = keccak256( 29 | abi.encode( 30 | keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), 31 | keccak256(bytes(name)), 32 | keccak256(bytes('1')), 33 | chainId, 34 | address(this) 35 | ) 36 | ); 37 | _mint(msg.sender, _totalSupply); 38 | } 39 | 40 | function _mint(address to, uint value) internal { 41 | totalSupply = totalSupply.add(value); 42 | balanceOf[to] = balanceOf[to].add(value); 43 | emit Transfer(address(0), to, value); 44 | } 45 | 46 | function _burn(address from, uint value) internal { 47 | balanceOf[from] = balanceOf[from].sub(value); 48 | totalSupply = totalSupply.sub(value); 49 | emit Transfer(from, address(0), value); 50 | } 51 | 52 | function _approve(address owner, address spender, uint value) private { 53 | allowance[owner][spender] = value; 54 | emit Approval(owner, spender, value); 55 | } 56 | 57 | function _transfer(address from, address to, uint value) private { 58 | uint burnAmount = value / 100; 59 | _burn(from, burnAmount); 60 | uint transferAmount = value.sub(burnAmount); 61 | balanceOf[from] = balanceOf[from].sub(transferAmount); 62 | balanceOf[to] = balanceOf[to].add(transferAmount); 63 | emit Transfer(from, to, transferAmount); 64 | } 65 | 66 | function approve(address spender, uint value) external returns (bool) { 67 | _approve(msg.sender, spender, value); 68 | return true; 69 | } 70 | 71 | function transfer(address to, uint value) external returns (bool) { 72 | _transfer(msg.sender, to, value); 73 | return true; 74 | } 75 | 76 | function transferFrom(address from, address to, uint value) external returns (bool) { 77 | if (allowance[from][msg.sender] != uint(-1)) { 78 | allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); 79 | } 80 | _transfer(from, to, value); 81 | return true; 82 | } 83 | 84 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 85 | require(deadline >= block.timestamp, 'EXPIRED'); 86 | bytes32 digest = keccak256( 87 | abi.encodePacked( 88 | '\x19\x01', 89 | DOMAIN_SEPARATOR, 90 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 91 | ) 92 | ); 93 | address recoveredAddress = ecrecover(digest, v, r, s); 94 | require(recoveredAddress != address(0) && recoveredAddress == owner, 'INVALID_SIGNATURE'); 95 | _approve(owner, spender, value); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /contracts/test/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '../libraries/SafeMath.sol'; 4 | 5 | contract ERC20 { 6 | using SafeMath for uint; 7 | 8 | string public constant name = 'Test Token'; 9 | string public constant symbol = 'TT'; 10 | uint8 public constant decimals = 18; 11 | uint public totalSupply; 12 | mapping(address => uint) public balanceOf; 13 | mapping(address => mapping(address => uint)) public allowance; 14 | 15 | bytes32 public DOMAIN_SEPARATOR; 16 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 17 | bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 18 | mapping(address => uint) public nonces; 19 | 20 | event Approval(address indexed owner, address indexed spender, uint value); 21 | event Transfer(address indexed from, address indexed to, uint value); 22 | 23 | constructor(uint _totalSupply) public { 24 | uint chainId; 25 | assembly { 26 | chainId := chainid() 27 | } 28 | DOMAIN_SEPARATOR = keccak256( 29 | abi.encode( 30 | keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), 31 | keccak256(bytes(name)), 32 | keccak256(bytes('1')), 33 | chainId, 34 | address(this) 35 | ) 36 | ); 37 | _mint(msg.sender, _totalSupply); 38 | } 39 | 40 | function _mint(address to, uint value) internal { 41 | totalSupply = totalSupply.add(value); 42 | balanceOf[to] = balanceOf[to].add(value); 43 | emit Transfer(address(0), to, value); 44 | } 45 | 46 | function _burn(address from, uint value) internal { 47 | balanceOf[from] = balanceOf[from].sub(value); 48 | totalSupply = totalSupply.sub(value); 49 | emit Transfer(from, address(0), value); 50 | } 51 | 52 | function _approve(address owner, address spender, uint value) private { 53 | allowance[owner][spender] = value; 54 | emit Approval(owner, spender, value); 55 | } 56 | 57 | function _transfer(address from, address to, uint value) private { 58 | balanceOf[from] = balanceOf[from].sub(value); 59 | balanceOf[to] = balanceOf[to].add(value); 60 | emit Transfer(from, to, value); 61 | } 62 | 63 | function approve(address spender, uint value) external returns (bool) { 64 | _approve(msg.sender, spender, value); 65 | return true; 66 | } 67 | 68 | function transfer(address to, uint value) external returns (bool) { 69 | _transfer(msg.sender, to, value); 70 | return true; 71 | } 72 | 73 | function transferFrom(address from, address to, uint value) external returns (bool) { 74 | if (allowance[from][msg.sender] != uint(-1)) { 75 | allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); 76 | } 77 | _transfer(from, to, value); 78 | return true; 79 | } 80 | 81 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 82 | require(deadline >= block.timestamp, 'EXPIRED'); 83 | bytes32 digest = keccak256( 84 | abi.encodePacked( 85 | '\x19\x01', 86 | DOMAIN_SEPARATOR, 87 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 88 | ) 89 | ); 90 | address recoveredAddress = ecrecover(digest, v, r, s); 91 | require(recoveredAddress != address(0) && recoveredAddress == owner, 'INVALID_SIGNATURE'); 92 | _approve(owner, spender, value); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/test/RouterEventEmitter.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.6.6; 2 | 3 | import '../interfaces/IUniswapV2Router01.sol'; 4 | 5 | contract RouterEventEmitter { 6 | event Amounts(uint[] amounts); 7 | 8 | receive() external payable {} 9 | 10 | function swapExactTokensForTokens( 11 | address router, 12 | uint amountIn, 13 | uint amountOutMin, 14 | address[] calldata path, 15 | address to, 16 | uint deadline 17 | ) external { 18 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 19 | IUniswapV2Router01(router).swapExactTokensForTokens.selector, amountIn, amountOutMin, path, to, deadline 20 | )); 21 | assert(success); 22 | emit Amounts(abi.decode(returnData, (uint[]))); 23 | } 24 | 25 | function swapTokensForExactTokens( 26 | address router, 27 | uint amountOut, 28 | uint amountInMax, 29 | address[] calldata path, 30 | address to, 31 | uint deadline 32 | ) external { 33 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 34 | IUniswapV2Router01(router).swapTokensForExactTokens.selector, amountOut, amountInMax, path, to, deadline 35 | )); 36 | assert(success); 37 | emit Amounts(abi.decode(returnData, (uint[]))); 38 | } 39 | 40 | function swapExactETHForTokens( 41 | address router, 42 | uint amountOutMin, 43 | address[] calldata path, 44 | address to, 45 | uint deadline 46 | ) external payable { 47 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 48 | IUniswapV2Router01(router).swapExactETHForTokens.selector, amountOutMin, path, to, deadline 49 | )); 50 | assert(success); 51 | emit Amounts(abi.decode(returnData, (uint[]))); 52 | } 53 | 54 | function swapTokensForExactETH( 55 | address router, 56 | uint amountOut, 57 | uint amountInMax, 58 | address[] calldata path, 59 | address to, 60 | uint deadline 61 | ) external { 62 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 63 | IUniswapV2Router01(router).swapTokensForExactETH.selector, amountOut, amountInMax, path, to, deadline 64 | )); 65 | assert(success); 66 | emit Amounts(abi.decode(returnData, (uint[]))); 67 | } 68 | 69 | function swapExactTokensForETH( 70 | address router, 71 | uint amountIn, 72 | uint amountOutMin, 73 | address[] calldata path, 74 | address to, 75 | uint deadline 76 | ) external { 77 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 78 | IUniswapV2Router01(router).swapExactTokensForETH.selector, amountIn, amountOutMin, path, to, deadline 79 | )); 80 | assert(success); 81 | emit Amounts(abi.decode(returnData, (uint[]))); 82 | } 83 | 84 | function swapETHForExactTokens( 85 | address router, 86 | uint amountOut, 87 | address[] calldata path, 88 | address to, 89 | uint deadline 90 | ) external payable { 91 | (bool success, bytes memory returnData) = router.delegatecall(abi.encodeWithSelector( 92 | IUniswapV2Router01(router).swapETHForExactTokens.selector, amountOut, path, to, deadline 93 | )); 94 | assert(success); 95 | emit Amounts(abi.decode(returnData, (uint[]))); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/v2-periphery", 3 | "version": "1.1.0-beta.0", 4 | "description": "🎚 Peripheral smart contracts for interacting with Uniswap V2", 5 | "engines": { 6 | "node": ">=10" 7 | }, 8 | "homepage": "https://uniswap.org", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Uniswap/uniswap-v2-periphery" 12 | }, 13 | "files": [ 14 | "build", 15 | "contracts" 16 | ], 17 | "dependencies": { 18 | "@uniswap/lib": "4.0.1-alpha", 19 | "@uniswap/v2-core": "1.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/chai": "^4.2.6", 23 | "@types/mocha": "^5.2.7", 24 | "chai": "^4.2.0", 25 | "ethereum-waffle": "^2.4.1", 26 | "ethereumjs-util": "^6.2.0", 27 | "mocha": "^6.2.2", 28 | "ncp": "^2.0.0", 29 | "prettier": "^1.19.1", 30 | "rimraf": "^3.0.0", 31 | "solc": "0.6.6", 32 | "ts-node": "^8.5.4", 33 | "typescript": "^3.7.3" 34 | }, 35 | "scripts": { 36 | "lint": "yarn prettier ./test/*.ts --check", 37 | "lint:fix": "yarn prettier ./test/*.ts --write", 38 | "clean": "rimraf ./build/", 39 | "copy-v1-artifacts": "ncp ./buildV1 ./build", 40 | "precompile": "yarn clean", 41 | "compile": "waffle .waffle.json", 42 | "postcompile": "yarn copy-v1-artifacts", 43 | "pretest": "yarn compile", 44 | "test": "mocha", 45 | "prepublishOnly": "yarn test" 46 | }, 47 | "license": "GPL-3.0-or-later" 48 | } 49 | -------------------------------------------------------------------------------- /test/ExampleComputeLiquidityValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { AddressZero, MaxUint256 } from 'ethers/constants' 2 | import chai, { expect } from 'chai' 3 | import { Contract } from 'ethers' 4 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 5 | 6 | import { expandTo18Decimals } from './shared/utilities' 7 | import { v2Fixture } from './shared/fixtures' 8 | 9 | import ExampleComputeLiquidityValue from '../build/ExampleComputeLiquidityValue.json' 10 | 11 | chai.use(solidity) 12 | 13 | const overrides = { 14 | gasLimit: 9999999 15 | } 16 | 17 | describe('ExampleComputeLiquidityValue', () => { 18 | const provider = new MockProvider({ 19 | hardfork: 'istanbul', 20 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 21 | gasLimit: 9999999 22 | }) 23 | const [wallet] = provider.getWallets() 24 | const loadFixture = createFixtureLoader(provider, [wallet]) 25 | 26 | let token0: Contract 27 | let token1: Contract 28 | let factory: Contract 29 | let pair: Contract 30 | let computeLiquidityValue: Contract 31 | let router: Contract 32 | beforeEach(async function() { 33 | const fixture = await loadFixture(v2Fixture) 34 | token0 = fixture.token0 35 | token1 = fixture.token1 36 | pair = fixture.pair 37 | factory = fixture.factoryV2 38 | router = fixture.router 39 | computeLiquidityValue = await deployContract( 40 | wallet, 41 | ExampleComputeLiquidityValue, 42 | [fixture.factoryV2.address], 43 | overrides 44 | ) 45 | }) 46 | 47 | beforeEach('mint some liquidity for the pair at 1:100 (100 shares minted)', async () => { 48 | await token0.transfer(pair.address, expandTo18Decimals(10)) 49 | await token1.transfer(pair.address, expandTo18Decimals(1000)) 50 | await pair.mint(wallet.address, overrides) 51 | expect(await pair.totalSupply()).to.eq(expandTo18Decimals(100)) 52 | }) 53 | 54 | it('correct factory address', async () => { 55 | expect(await computeLiquidityValue.factory()).to.eq(factory.address) 56 | }) 57 | 58 | describe('#getLiquidityValue', () => { 59 | it('correct for 5 shares', async () => { 60 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValue( 61 | token0.address, 62 | token1.address, 63 | expandTo18Decimals(5) 64 | ) 65 | expect(token0Amount).to.eq('500000000000000000') 66 | expect(token1Amount).to.eq('50000000000000000000') 67 | }) 68 | it('correct for 7 shares', async () => { 69 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValue( 70 | token0.address, 71 | token1.address, 72 | expandTo18Decimals(7) 73 | ) 74 | expect(token0Amount).to.eq('700000000000000000') 75 | expect(token1Amount).to.eq('70000000000000000000') 76 | }) 77 | 78 | it('correct after swap', async () => { 79 | await token0.approve(router.address, MaxUint256, overrides) 80 | await router.swapExactTokensForTokens( 81 | expandTo18Decimals(10), 82 | 0, 83 | [token0.address, token1.address], 84 | wallet.address, 85 | MaxUint256, 86 | overrides 87 | ) 88 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValue( 89 | token0.address, 90 | token1.address, 91 | expandTo18Decimals(7) 92 | ) 93 | expect(token0Amount).to.eq('1400000000000000000') 94 | expect(token1Amount).to.eq('35052578868302453680') 95 | }) 96 | 97 | describe('fee on', () => { 98 | beforeEach('turn on fee', async () => { 99 | await factory.setFeeTo(wallet.address) 100 | }) 101 | 102 | // this is necessary to cause kLast to be set 103 | beforeEach('mint more liquidity to address zero', async () => { 104 | await token0.transfer(pair.address, expandTo18Decimals(10)) 105 | await token1.transfer(pair.address, expandTo18Decimals(1000)) 106 | await pair.mint(AddressZero, overrides) 107 | expect(await pair.totalSupply()).to.eq(expandTo18Decimals(200)) 108 | }) 109 | 110 | it('correct after swap', async () => { 111 | await token0.approve(router.address, MaxUint256, overrides) 112 | await router.swapExactTokensForTokens( 113 | expandTo18Decimals(20), 114 | 0, 115 | [token0.address, token1.address], 116 | wallet.address, 117 | MaxUint256, 118 | overrides 119 | ) 120 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValue( 121 | token0.address, 122 | token1.address, 123 | expandTo18Decimals(7) 124 | ) 125 | expect(token0Amount).to.eq('1399824934325735058') 126 | expect(token1Amount).to.eq('35048195651620807684') 127 | }) 128 | }) 129 | }) 130 | 131 | describe('#getReservesAfterArbitrage', () => { 132 | it('1/400', async () => { 133 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 134 | token0.address, 135 | token1.address, 136 | 1, 137 | 400 138 | ) 139 | expect(reserveA).to.eq('5007516917298542016') 140 | expect(reserveB).to.eq('1999997739838173075192') 141 | }) 142 | it('1/200', async () => { 143 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 144 | token0.address, 145 | token1.address, 146 | 1, 147 | 200 148 | ) 149 | expect(reserveA).to.eq('7081698338256310291') 150 | expect(reserveB).to.eq('1413330640570018326894') 151 | }) 152 | it('1/100 (same price)', async () => { 153 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 154 | token0.address, 155 | token1.address, 156 | 1, 157 | 100 158 | ) 159 | expect(reserveA).to.eq('10000000000000000000') 160 | expect(reserveB).to.eq('1000000000000000000000') 161 | }) 162 | it('1/50', async () => { 163 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 164 | token0.address, 165 | token1.address, 166 | 1, 167 | 50 168 | ) 169 | expect(reserveA).to.eq('14133306405700183269') 170 | expect(reserveB).to.eq('708169833825631029041') 171 | }) 172 | it('1/25', async () => { 173 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 174 | token0.address, 175 | token1.address, 176 | 1, 177 | 25 178 | ) 179 | expect(reserveA).to.eq('19999977398381730752') 180 | expect(reserveB).to.eq('500751691729854201595') 181 | }) 182 | it('25/1', async () => { 183 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 184 | token0.address, 185 | token1.address, 186 | 25, 187 | 1 188 | ) 189 | expect(reserveA).to.eq('500721601459041764285') 190 | expect(reserveB).to.eq('20030067669194168064') 191 | }) 192 | it('works with large numbers for the price', async () => { 193 | const [reserveA, reserveB] = await computeLiquidityValue.getReservesAfterArbitrage( 194 | token0.address, 195 | token1.address, 196 | MaxUint256.div(1000), 197 | MaxUint256.div(1000) 198 | ) 199 | // diff of 30 bips 200 | expect(reserveA).to.eq('100120248075158403008') 201 | expect(reserveB).to.eq('100150338345970840319') 202 | }) 203 | }) 204 | 205 | describe('#getLiquidityValue', () => { 206 | describe('fee is off', () => { 207 | it('produces the correct value after arbing to 1:105', async () => { 208 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 209 | token0.address, 210 | token1.address, 211 | 1, 212 | 105, 213 | expandTo18Decimals(5) 214 | ) 215 | expect(token0Amount).to.eq('488683612488266114') // slightly less than 5% of 10, or 0.5 216 | expect(token1Amount).to.eq('51161327957205755422') // slightly more than 5% of 100, or 5 217 | }) 218 | 219 | it('produces the correct value after arbing to 1:95', async () => { 220 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 221 | token0.address, 222 | token1.address, 223 | 1, 224 | 95, 225 | expandTo18Decimals(5) 226 | ) 227 | expect(token0Amount).to.eq('512255881944227034') // slightly more than 5% of 10, or 0.5 228 | expect(token1Amount).to.eq('48807237571060645526') // slightly less than 5% of 100, or 5 229 | }) 230 | 231 | it('produces correct value at the current price', async () => { 232 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 233 | token0.address, 234 | token1.address, 235 | 1, 236 | 100, 237 | expandTo18Decimals(5) 238 | ) 239 | expect(token0Amount).to.eq('500000000000000000') 240 | expect(token1Amount).to.eq('50000000000000000000') 241 | }) 242 | 243 | it('gas current price', async () => { 244 | expect( 245 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 246 | token0.address, 247 | token1.address, 248 | 1, 249 | 100, 250 | expandTo18Decimals(5) 251 | ) 252 | ).to.eq('12705') 253 | }) 254 | 255 | it('gas higher price', async () => { 256 | expect( 257 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 258 | token0.address, 259 | token1.address, 260 | 1, 261 | 105, 262 | expandTo18Decimals(5) 263 | ) 264 | ).to.eq('13478') 265 | }) 266 | 267 | it('gas lower price', async () => { 268 | expect( 269 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 270 | token0.address, 271 | token1.address, 272 | 1, 273 | 95, 274 | expandTo18Decimals(5) 275 | ) 276 | ).to.eq('13523') 277 | }) 278 | 279 | describe('after a swap', () => { 280 | beforeEach('swap to ~1:25', async () => { 281 | await token0.approve(router.address, MaxUint256, overrides) 282 | await router.swapExactTokensForTokens( 283 | expandTo18Decimals(10), 284 | 0, 285 | [token0.address, token1.address], 286 | wallet.address, 287 | MaxUint256, 288 | overrides 289 | ) 290 | const [reserve0, reserve1] = await pair.getReserves() 291 | expect(reserve0).to.eq('20000000000000000000') 292 | expect(reserve1).to.eq('500751126690035052579') // half plus the fee 293 | }) 294 | 295 | it('is roughly 1/25th liquidity', async () => { 296 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 297 | token0.address, 298 | token1.address, 299 | 1, 300 | 25, 301 | expandTo18Decimals(5) 302 | ) 303 | 304 | expect(token0Amount).to.eq('1000000000000000000') 305 | expect(token1Amount).to.eq('25037556334501752628') 306 | }) 307 | 308 | it('shares after arbing back to 1:100', async () => { 309 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 310 | token0.address, 311 | token1.address, 312 | 1, 313 | 100, 314 | expandTo18Decimals(5) 315 | ) 316 | 317 | expect(token0Amount).to.eq('501127678536722155') 318 | expect(token1Amount).to.eq('50037429168613534246') 319 | }) 320 | }) 321 | }) 322 | 323 | describe('fee is on', () => { 324 | beforeEach('turn on fee', async () => { 325 | await factory.setFeeTo(wallet.address) 326 | }) 327 | 328 | // this is necessary to cause kLast to be set 329 | beforeEach('mint more liquidity to address zero', async () => { 330 | await token0.transfer(pair.address, expandTo18Decimals(10)) 331 | await token1.transfer(pair.address, expandTo18Decimals(1000)) 332 | await pair.mint(AddressZero, overrides) 333 | expect(await pair.totalSupply()).to.eq(expandTo18Decimals(200)) 334 | }) 335 | 336 | describe('no fee to be collected', () => { 337 | it('produces the correct value after arbing to 1:105', async () => { 338 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 339 | token0.address, 340 | token1.address, 341 | 1, 342 | 105, 343 | expandTo18Decimals(5) 344 | ) 345 | expect(token0Amount).to.eq('488680839243189328') // slightly less than 5% of 10, or 0.5 346 | expect(token1Amount).to.eq('51161037620273529068') // slightly more than 5% of 100, or 5 347 | }) 348 | 349 | it('produces the correct value after arbing to 1:95', async () => { 350 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 351 | token0.address, 352 | token1.address, 353 | 1, 354 | 95, 355 | expandTo18Decimals(5) 356 | ) 357 | expect(token0Amount).to.eq('512252817918759166') // slightly more than 5% of 10, or 0.5 358 | expect(token1Amount).to.eq('48806945633721895174') // slightly less than 5% of 100, or 5 359 | }) 360 | 361 | it('produces correct value at the current price', async () => { 362 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 363 | token0.address, 364 | token1.address, 365 | 1, 366 | 100, 367 | expandTo18Decimals(5) 368 | ) 369 | expect(token0Amount).to.eq('500000000000000000') 370 | expect(token1Amount).to.eq('50000000000000000000') 371 | }) 372 | }) 373 | 374 | it('gas current price', async () => { 375 | expect( 376 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 377 | token0.address, 378 | token1.address, 379 | 1, 380 | 100, 381 | expandTo18Decimals(5) 382 | ) 383 | ).to.eq('16938') 384 | }) 385 | 386 | it('gas higher price', async () => { 387 | expect( 388 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 389 | token0.address, 390 | token1.address, 391 | 1, 392 | 105, 393 | expandTo18Decimals(5) 394 | ) 395 | ).to.eq('18475') 396 | }) 397 | 398 | it('gas lower price', async () => { 399 | expect( 400 | await computeLiquidityValue.getGasCostOfGetLiquidityValueAfterArbitrageToPrice( 401 | token0.address, 402 | token1.address, 403 | 1, 404 | 95, 405 | expandTo18Decimals(5) 406 | ) 407 | ).to.eq('18406') 408 | }) 409 | 410 | describe('after a swap', () => { 411 | beforeEach('swap to ~1:25', async () => { 412 | await token0.approve(router.address, MaxUint256, overrides) 413 | await router.swapExactTokensForTokens( 414 | expandTo18Decimals(20), 415 | 0, 416 | [token0.address, token1.address], 417 | wallet.address, 418 | MaxUint256, 419 | overrides 420 | ) 421 | const [reserve0, reserve1] = await pair.getReserves() 422 | expect(reserve0).to.eq('40000000000000000000') 423 | expect(reserve1).to.eq('1001502253380070105158') // half plus the fee 424 | }) 425 | 426 | it('is roughly 1:25', async () => { 427 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 428 | token0.address, 429 | token1.address, 430 | 1, 431 | 25, 432 | expandTo18Decimals(5) 433 | ) 434 | 435 | expect(token0Amount).to.eq('999874953089810756') 436 | expect(token1Amount).to.eq('25034425465443434060') 437 | }) 438 | 439 | it('shares after arbing back to 1:100', async () => { 440 | const [token0Amount, token1Amount] = await computeLiquidityValue.getLiquidityValueAfterArbitrageToPrice( 441 | token0.address, 442 | token1.address, 443 | 1, 444 | 100, 445 | expandTo18Decimals(5) 446 | ) 447 | 448 | expect(token0Amount).to.eq('501002443792372662') 449 | expect(token1Amount).to.eq('50024924521757597314') 450 | }) 451 | }) 452 | }) 453 | }) 454 | }) 455 | -------------------------------------------------------------------------------- /test/ExampleFlashSwap.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { MaxUint256 } from 'ethers/constants' 4 | import { BigNumber, bigNumberify, defaultAbiCoder, formatEther } from 'ethers/utils' 5 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 6 | 7 | import { expandTo18Decimals } from './shared/utilities' 8 | import { v2Fixture } from './shared/fixtures' 9 | 10 | import ExampleFlashSwap from '../build/ExampleFlashSwap.json' 11 | 12 | chai.use(solidity) 13 | 14 | const overrides = { 15 | gasLimit: 9999999, 16 | gasPrice: 0 17 | } 18 | 19 | describe('ExampleFlashSwap', () => { 20 | const provider = new MockProvider({ 21 | hardfork: 'istanbul', 22 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 23 | gasLimit: 9999999 24 | }) 25 | const [wallet] = provider.getWallets() 26 | const loadFixture = createFixtureLoader(provider, [wallet]) 27 | 28 | let WETH: Contract 29 | let WETHPartner: Contract 30 | let WETHExchangeV1: Contract 31 | let WETHPair: Contract 32 | let flashSwapExample: Contract 33 | beforeEach(async function() { 34 | const fixture = await loadFixture(v2Fixture) 35 | 36 | WETH = fixture.WETH 37 | WETHPartner = fixture.WETHPartner 38 | WETHExchangeV1 = fixture.WETHExchangeV1 39 | WETHPair = fixture.WETHPair 40 | flashSwapExample = await deployContract( 41 | wallet, 42 | ExampleFlashSwap, 43 | [fixture.factoryV2.address, fixture.factoryV1.address, fixture.router.address], 44 | overrides 45 | ) 46 | }) 47 | 48 | it('uniswapV2Call:0', async () => { 49 | // add liquidity to V1 at a rate of 1 ETH / 200 X 50 | const WETHPartnerAmountV1 = expandTo18Decimals(2000) 51 | const ETHAmountV1 = expandTo18Decimals(10) 52 | await WETHPartner.approve(WETHExchangeV1.address, WETHPartnerAmountV1) 53 | await WETHExchangeV1.addLiquidity(bigNumberify(1), WETHPartnerAmountV1, MaxUint256, { 54 | ...overrides, 55 | value: ETHAmountV1 56 | }) 57 | 58 | // add liquidity to V2 at a rate of 1 ETH / 100 X 59 | const WETHPartnerAmountV2 = expandTo18Decimals(1000) 60 | const ETHAmountV2 = expandTo18Decimals(10) 61 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmountV2) 62 | await WETH.deposit({ value: ETHAmountV2 }) 63 | await WETH.transfer(WETHPair.address, ETHAmountV2) 64 | await WETHPair.mint(wallet.address, overrides) 65 | 66 | const balanceBefore = await WETHPartner.balanceOf(wallet.address) 67 | 68 | // now, execute arbitrage via uniswapV2Call: 69 | // receive 1 ETH from V2, get as much X from V1 as we can, repay V2 with minimum X, keep the rest! 70 | const arbitrageAmount = expandTo18Decimals(1) 71 | // instead of being 'hard-coded', the above value could be calculated optimally off-chain. this would be 72 | // better, but it'd be better yet to calculate the amount at runtime, on-chain. unfortunately, this requires a 73 | // swap-to-price calculation, which is a little tricky, and out of scope for the moment 74 | const WETHPairToken0 = await WETHPair.token0() 75 | const amount0 = WETHPairToken0 === WETHPartner.address ? bigNumberify(0) : arbitrageAmount 76 | const amount1 = WETHPairToken0 === WETHPartner.address ? arbitrageAmount : bigNumberify(0) 77 | await WETHPair.swap( 78 | amount0, 79 | amount1, 80 | flashSwapExample.address, 81 | defaultAbiCoder.encode(['uint'], [bigNumberify(1)]), 82 | overrides 83 | ) 84 | 85 | const balanceAfter = await WETHPartner.balanceOf(wallet.address) 86 | const profit = balanceAfter.sub(balanceBefore).div(expandTo18Decimals(1)) 87 | const reservesV1 = [ 88 | await WETHPartner.balanceOf(WETHExchangeV1.address), 89 | await provider.getBalance(WETHExchangeV1.address) 90 | ] 91 | const priceV1 = reservesV1[0].div(reservesV1[1]) 92 | const reservesV2 = (await WETHPair.getReserves()).slice(0, 2) 93 | const priceV2 = 94 | WETHPairToken0 === WETHPartner.address ? reservesV2[0].div(reservesV2[1]) : reservesV2[1].div(reservesV2[0]) 95 | 96 | expect(profit.toString()).to.eq('69') // our profit is ~69 tokens 97 | expect(priceV1.toString()).to.eq('165') // we pushed the v1 price down to ~165 98 | expect(priceV2.toString()).to.eq('123') // we pushed the v2 price up to ~123 99 | }) 100 | 101 | it('uniswapV2Call:1', async () => { 102 | // add liquidity to V1 at a rate of 1 ETH / 100 X 103 | const WETHPartnerAmountV1 = expandTo18Decimals(1000) 104 | const ETHAmountV1 = expandTo18Decimals(10) 105 | await WETHPartner.approve(WETHExchangeV1.address, WETHPartnerAmountV1) 106 | await WETHExchangeV1.addLiquidity(bigNumberify(1), WETHPartnerAmountV1, MaxUint256, { 107 | ...overrides, 108 | value: ETHAmountV1 109 | }) 110 | 111 | // add liquidity to V2 at a rate of 1 ETH / 200 X 112 | const WETHPartnerAmountV2 = expandTo18Decimals(2000) 113 | const ETHAmountV2 = expandTo18Decimals(10) 114 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmountV2) 115 | await WETH.deposit({ value: ETHAmountV2 }) 116 | await WETH.transfer(WETHPair.address, ETHAmountV2) 117 | await WETHPair.mint(wallet.address, overrides) 118 | 119 | const balanceBefore = await provider.getBalance(wallet.address) 120 | 121 | // now, execute arbitrage via uniswapV2Call: 122 | // receive 200 X from V2, get as much ETH from V1 as we can, repay V2 with minimum ETH, keep the rest! 123 | const arbitrageAmount = expandTo18Decimals(200) 124 | // instead of being 'hard-coded', the above value could be calculated optimally off-chain. this would be 125 | // better, but it'd be better yet to calculate the amount at runtime, on-chain. unfortunately, this requires a 126 | // swap-to-price calculation, which is a little tricky, and out of scope for the moment 127 | const WETHPairToken0 = await WETHPair.token0() 128 | const amount0 = WETHPairToken0 === WETHPartner.address ? arbitrageAmount : bigNumberify(0) 129 | const amount1 = WETHPairToken0 === WETHPartner.address ? bigNumberify(0) : arbitrageAmount 130 | await WETHPair.swap( 131 | amount0, 132 | amount1, 133 | flashSwapExample.address, 134 | defaultAbiCoder.encode(['uint'], [bigNumberify(1)]), 135 | overrides 136 | ) 137 | 138 | const balanceAfter = await provider.getBalance(wallet.address) 139 | const profit = balanceAfter.sub(balanceBefore) 140 | const reservesV1 = [ 141 | await WETHPartner.balanceOf(WETHExchangeV1.address), 142 | await provider.getBalance(WETHExchangeV1.address) 143 | ] 144 | const priceV1 = reservesV1[0].div(reservesV1[1]) 145 | const reservesV2 = (await WETHPair.getReserves()).slice(0, 2) 146 | const priceV2 = 147 | WETHPairToken0 === WETHPartner.address ? reservesV2[0].div(reservesV2[1]) : reservesV2[1].div(reservesV2[0]) 148 | 149 | expect(formatEther(profit)).to.eq('0.548043441089763649') // our profit is ~.5 ETH 150 | expect(priceV1.toString()).to.eq('143') // we pushed the v1 price up to ~143 151 | expect(priceV2.toString()).to.eq('161') // we pushed the v2 price down to ~161 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /test/ExampleOracleSimple.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { BigNumber } from 'ethers/utils' 4 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 5 | 6 | import { expandTo18Decimals, mineBlock, encodePrice } from './shared/utilities' 7 | import { v2Fixture } from './shared/fixtures' 8 | 9 | import ExampleOracleSimple from '../build/ExampleOracleSimple.json' 10 | 11 | chai.use(solidity) 12 | 13 | const overrides = { 14 | gasLimit: 9999999 15 | } 16 | 17 | const token0Amount = expandTo18Decimals(5) 18 | const token1Amount = expandTo18Decimals(10) 19 | 20 | describe('ExampleOracleSimple', () => { 21 | const provider = new MockProvider({ 22 | hardfork: 'istanbul', 23 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 24 | gasLimit: 9999999 25 | }) 26 | const [wallet] = provider.getWallets() 27 | const loadFixture = createFixtureLoader(provider, [wallet]) 28 | 29 | let token0: Contract 30 | let token1: Contract 31 | let pair: Contract 32 | let exampleOracleSimple: Contract 33 | 34 | async function addLiquidity() { 35 | await token0.transfer(pair.address, token0Amount) 36 | await token1.transfer(pair.address, token1Amount) 37 | await pair.mint(wallet.address, overrides) 38 | } 39 | 40 | beforeEach(async function() { 41 | const fixture = await loadFixture(v2Fixture) 42 | 43 | token0 = fixture.token0 44 | token1 = fixture.token1 45 | pair = fixture.pair 46 | await addLiquidity() 47 | exampleOracleSimple = await deployContract( 48 | wallet, 49 | ExampleOracleSimple, 50 | [fixture.factoryV2.address, token0.address, token1.address], 51 | overrides 52 | ) 53 | }) 54 | 55 | it('update', async () => { 56 | const blockTimestamp = (await pair.getReserves())[2] 57 | await mineBlock(provider, blockTimestamp + 60 * 60 * 23) 58 | await expect(exampleOracleSimple.update(overrides)).to.be.reverted 59 | await mineBlock(provider, blockTimestamp + 60 * 60 * 24) 60 | await exampleOracleSimple.update(overrides) 61 | 62 | const expectedPrice = encodePrice(token0Amount, token1Amount) 63 | 64 | expect(await exampleOracleSimple.price0Average()).to.eq(expectedPrice[0]) 65 | expect(await exampleOracleSimple.price1Average()).to.eq(expectedPrice[1]) 66 | 67 | expect(await exampleOracleSimple.consult(token0.address, token0Amount)).to.eq(token1Amount) 68 | expect(await exampleOracleSimple.consult(token1.address, token1Amount)).to.eq(token0Amount) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/ExampleSlidingWindowOracle.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { BigNumber, bigNumberify } from 'ethers/utils' 4 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 5 | 6 | import { expandTo18Decimals, mineBlock, encodePrice } from './shared/utilities' 7 | import { v2Fixture } from './shared/fixtures' 8 | 9 | import ExampleSlidingWindowOracle from '../build/ExampleSlidingWindowOracle.json' 10 | 11 | chai.use(solidity) 12 | 13 | const overrides = { 14 | gasLimit: 9999999 15 | } 16 | 17 | const defaultToken0Amount = expandTo18Decimals(5) 18 | const defaultToken1Amount = expandTo18Decimals(10) 19 | 20 | describe('ExampleSlidingWindowOracle', () => { 21 | const provider = new MockProvider({ 22 | hardfork: 'istanbul', 23 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 24 | gasLimit: 9999999 25 | }) 26 | const [wallet] = provider.getWallets() 27 | const loadFixture = createFixtureLoader(provider, [wallet]) 28 | 29 | let token0: Contract 30 | let token1: Contract 31 | let pair: Contract 32 | let weth: Contract 33 | let factory: Contract 34 | 35 | async function addLiquidity(amount0: BigNumber = defaultToken0Amount, amount1: BigNumber = defaultToken1Amount) { 36 | if (!amount0.isZero()) await token0.transfer(pair.address, amount0) 37 | if (!amount1.isZero()) await token1.transfer(pair.address, amount1) 38 | await pair.sync() 39 | } 40 | 41 | const defaultWindowSize = 86400 // 24 hours 42 | const defaultGranularity = 24 // 1 hour each 43 | 44 | function observationIndexOf( 45 | timestamp: number, 46 | windowSize: number = defaultWindowSize, 47 | granularity: number = defaultGranularity 48 | ): number { 49 | const periodSize = Math.floor(windowSize / granularity) 50 | const epochPeriod = Math.floor(timestamp / periodSize) 51 | return epochPeriod % granularity 52 | } 53 | 54 | function deployOracle(windowSize: number, granularity: number) { 55 | return deployContract(wallet, ExampleSlidingWindowOracle, [factory.address, windowSize, granularity], overrides) 56 | } 57 | 58 | beforeEach('deploy fixture', async function() { 59 | const fixture = await loadFixture(v2Fixture) 60 | 61 | token0 = fixture.token0 62 | token1 = fixture.token1 63 | pair = fixture.pair 64 | weth = fixture.WETH 65 | factory = fixture.factoryV2 66 | }) 67 | 68 | // 1/1/2020 @ 12:00 am UTC 69 | // cannot be 0 because that instructs ganache to set it to current timestamp 70 | // cannot be 86400 because then timestamp 0 is a valid historical observation 71 | const startTime = 1577836800 72 | 73 | // must come before adding liquidity to pairs for correct cumulative price computations 74 | // cannot use 0 because that resets to current timestamp 75 | beforeEach(`set start time to ${startTime}`, () => mineBlock(provider, startTime)) 76 | 77 | it('requires granularity to be greater than 0', async () => { 78 | await expect(deployOracle(defaultWindowSize, 0)).to.be.revertedWith('SlidingWindowOracle: GRANULARITY') 79 | }) 80 | 81 | it('requires windowSize to be evenly divisible by granularity', async () => { 82 | await expect(deployOracle(defaultWindowSize - 1, defaultGranularity)).to.be.revertedWith( 83 | 'SlidingWindowOracle: WINDOW_NOT_EVENLY_DIVISIBLE' 84 | ) 85 | }) 86 | 87 | it('computes the periodSize correctly', async () => { 88 | const oracle = await deployOracle(defaultWindowSize, defaultGranularity) 89 | expect(await oracle.periodSize()).to.eq(3600) 90 | const oracleOther = await deployOracle(defaultWindowSize * 2, defaultGranularity / 2) 91 | expect(await oracleOther.periodSize()).to.eq(3600 * 4) 92 | }) 93 | 94 | describe('#observationIndexOf', () => { 95 | it('works for examples', async () => { 96 | const oracle = await deployOracle(defaultWindowSize, defaultGranularity) 97 | expect(await oracle.observationIndexOf(0)).to.eq(0) 98 | expect(await oracle.observationIndexOf(3599)).to.eq(0) 99 | expect(await oracle.observationIndexOf(3600)).to.eq(1) 100 | expect(await oracle.observationIndexOf(4800)).to.eq(1) 101 | expect(await oracle.observationIndexOf(7199)).to.eq(1) 102 | expect(await oracle.observationIndexOf(7200)).to.eq(2) 103 | expect(await oracle.observationIndexOf(86399)).to.eq(23) 104 | expect(await oracle.observationIndexOf(86400)).to.eq(0) 105 | expect(await oracle.observationIndexOf(90000)).to.eq(1) 106 | }) 107 | it('overflow safe', async () => { 108 | const oracle = await deployOracle(25500, 255) // 100 period size 109 | expect(await oracle.observationIndexOf(0)).to.eq(0) 110 | expect(await oracle.observationIndexOf(99)).to.eq(0) 111 | expect(await oracle.observationIndexOf(100)).to.eq(1) 112 | expect(await oracle.observationIndexOf(199)).to.eq(1) 113 | expect(await oracle.observationIndexOf(25499)).to.eq(254) // 255th element 114 | expect(await oracle.observationIndexOf(25500)).to.eq(0) 115 | }) 116 | it('matches offline computation', async () => { 117 | const oracle = await deployOracle(defaultWindowSize, defaultGranularity) 118 | for (let timestamp of [0, 5000, 1000, 25000, 86399, 86400, 86401]) { 119 | expect(await oracle.observationIndexOf(timestamp)).to.eq(observationIndexOf(timestamp)) 120 | } 121 | }) 122 | }) 123 | 124 | describe('#update', () => { 125 | let slidingWindowOracle: Contract 126 | 127 | beforeEach( 128 | 'deploy oracle', 129 | async () => (slidingWindowOracle = await deployOracle(defaultWindowSize, defaultGranularity)) 130 | ) 131 | 132 | beforeEach('add default liquidity', () => addLiquidity()) 133 | 134 | it('succeeds', async () => { 135 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 136 | }) 137 | 138 | it('sets the appropriate epoch slot', async () => { 139 | const blockTimestamp = (await pair.getReserves())[2] 140 | expect(blockTimestamp).to.eq(startTime) 141 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 142 | expect(await slidingWindowOracle.pairObservations(pair.address, observationIndexOf(blockTimestamp))).to.deep.eq([ 143 | bigNumberify(blockTimestamp), 144 | await pair.price0CumulativeLast(), 145 | await pair.price1CumulativeLast() 146 | ]) 147 | }).retries(2) // we may have slight differences between pair blockTimestamp and the expected timestamp 148 | // because the previous block timestamp may differ from the current block timestamp by 1 second 149 | 150 | it('gas for first update (allocates empty array)', async () => { 151 | const tx = await slidingWindowOracle.update(token0.address, token1.address, overrides) 152 | const receipt = await tx.wait() 153 | expect(receipt.gasUsed).to.eq('116816') 154 | }).retries(2) // gas test inconsistent 155 | 156 | it('gas for second update in the same period (skips)', async () => { 157 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 158 | const tx = await slidingWindowOracle.update(token0.address, token1.address, overrides) 159 | const receipt = await tx.wait() 160 | expect(receipt.gasUsed).to.eq('25574') 161 | }).retries(2) // gas test inconsistent 162 | 163 | it('gas for second update different period (no allocate, no skip)', async () => { 164 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 165 | await mineBlock(provider, startTime + 3600) 166 | const tx = await slidingWindowOracle.update(token0.address, token1.address, overrides) 167 | const receipt = await tx.wait() 168 | expect(receipt.gasUsed).to.eq('94703') 169 | }).retries(2) // gas test inconsistent 170 | 171 | it('second update in one timeslot does not overwrite', async () => { 172 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 173 | const before = await slidingWindowOracle.pairObservations(pair.address, observationIndexOf(0)) 174 | // first hour still 175 | await mineBlock(provider, startTime + 1800) 176 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 177 | const after = await slidingWindowOracle.pairObservations(pair.address, observationIndexOf(1800)) 178 | expect(observationIndexOf(1800)).to.eq(observationIndexOf(0)) 179 | expect(before).to.deep.eq(after) 180 | }) 181 | 182 | it('fails for invalid pair', async () => { 183 | await expect(slidingWindowOracle.update(weth.address, token1.address)).to.be.reverted 184 | }) 185 | }) 186 | 187 | describe('#consult', () => { 188 | let slidingWindowOracle: Contract 189 | 190 | beforeEach( 191 | 'deploy oracle', 192 | async () => (slidingWindowOracle = await deployOracle(defaultWindowSize, defaultGranularity)) 193 | ) 194 | 195 | // must come after setting time to 0 for correct cumulative price computations in the pair 196 | beforeEach('add default liquidity', () => addLiquidity()) 197 | 198 | it('fails if previous bucket not set', async () => { 199 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 200 | await expect(slidingWindowOracle.consult(token0.address, 0, token1.address)).to.be.revertedWith( 201 | 'SlidingWindowOracle: MISSING_HISTORICAL_OBSERVATION' 202 | ) 203 | }) 204 | 205 | it('fails for invalid pair', async () => { 206 | await expect(slidingWindowOracle.consult(weth.address, 0, token1.address)).to.be.reverted 207 | }) 208 | 209 | describe('happy path', () => { 210 | let blockTimestamp: number 211 | let previousBlockTimestamp: number 212 | let previousCumulativePrices: any 213 | beforeEach('add some prices', async () => { 214 | previousBlockTimestamp = (await pair.getReserves())[2] 215 | previousCumulativePrices = [await pair.price0CumulativeLast(), await pair.price1CumulativeLast()] 216 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 217 | blockTimestamp = previousBlockTimestamp + 23 * 3600 218 | await mineBlock(provider, blockTimestamp) 219 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 220 | }) 221 | 222 | it('has cumulative price in previous bucket', async () => { 223 | expect( 224 | await slidingWindowOracle.pairObservations(pair.address, observationIndexOf(previousBlockTimestamp)) 225 | ).to.deep.eq([bigNumberify(previousBlockTimestamp), previousCumulativePrices[0], previousCumulativePrices[1]]) 226 | }).retries(5) // test flaky because timestamps aren't mocked 227 | 228 | it('has cumulative price in current bucket', async () => { 229 | const timeElapsed = blockTimestamp - previousBlockTimestamp 230 | const prices = encodePrice(defaultToken0Amount, defaultToken1Amount) 231 | expect( 232 | await slidingWindowOracle.pairObservations(pair.address, observationIndexOf(blockTimestamp)) 233 | ).to.deep.eq([bigNumberify(blockTimestamp), prices[0].mul(timeElapsed), prices[1].mul(timeElapsed)]) 234 | }).retries(5) // test flaky because timestamps aren't mocked 235 | 236 | it('provides the current ratio in consult token0', async () => { 237 | expect(await slidingWindowOracle.consult(token0.address, 100, token1.address)).to.eq(200) 238 | }) 239 | 240 | it('provides the current ratio in consult token1', async () => { 241 | expect(await slidingWindowOracle.consult(token1.address, 100, token0.address)).to.eq(50) 242 | }) 243 | }) 244 | 245 | describe('price changes over period', () => { 246 | const hour = 3600 247 | beforeEach('add some prices', async () => { 248 | // starting price of 1:2, or token0 = 2token1, token1 = 0.5token0 249 | await slidingWindowOracle.update(token0.address, token1.address, overrides) // hour 0, 1:2 250 | // change the price at hour 3 to 1:1 and immediately update 251 | await mineBlock(provider, startTime + 3 * hour) 252 | await addLiquidity(defaultToken0Amount, bigNumberify(0)) 253 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 254 | 255 | // change the ratios at hour 6:00 to 2:1, don't update right away 256 | await mineBlock(provider, startTime + 6 * hour) 257 | await token0.transfer(pair.address, defaultToken0Amount.mul(2)) 258 | await pair.sync() 259 | 260 | // update at hour 9:00 (price has been 2:1 for 3 hours, invokes counterfactual) 261 | await mineBlock(provider, startTime + 9 * hour) 262 | await slidingWindowOracle.update(token0.address, token1.address, overrides) 263 | // move to hour 23:00 so we can check prices 264 | await mineBlock(provider, startTime + 23 * hour) 265 | }) 266 | 267 | it('provides the correct ratio in consult token0', async () => { 268 | // at hour 23, price of token 0 spent 3 hours at 2, 3 hours at 1, 17 hours at 0.5 so price should 269 | // be less than 1 270 | expect(await slidingWindowOracle.consult(token0.address, 100, token1.address)).to.eq(76) 271 | }) 272 | 273 | it('provides the correct ratio in consult token1', async () => { 274 | // price should be greater than 1 275 | expect(await slidingWindowOracle.consult(token1.address, 100, token0.address)).to.eq(167) 276 | }) 277 | 278 | // price has been 2:1 all of 23 hours 279 | describe('hour 32', () => { 280 | beforeEach('set hour 32', () => mineBlock(provider, startTime + 32 * hour)) 281 | it('provides the correct ratio in consult token0', async () => { 282 | // at hour 23, price of token 0 spent 3 hours at 2, 3 hours at 1, 17 hours at 0.5 so price should 283 | // be less than 1 284 | expect(await slidingWindowOracle.consult(token0.address, 100, token1.address)).to.eq(50) 285 | }) 286 | 287 | it('provides the correct ratio in consult token1', async () => { 288 | // price should be greater than 1 289 | expect(await slidingWindowOracle.consult(token1.address, 100, token0.address)).to.eq(200) 290 | }) 291 | }) 292 | }) 293 | }) 294 | }) 295 | -------------------------------------------------------------------------------- /test/ExampleSwapToPrice.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { MaxUint256 } from 'ethers/constants' 4 | import { BigNumber, bigNumberify, defaultAbiCoder, formatEther } from 'ethers/utils' 5 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 6 | 7 | import { expandTo18Decimals } from './shared/utilities' 8 | import { v2Fixture } from './shared/fixtures' 9 | 10 | import ExampleSwapToPrice from '../build/ExampleSwapToPrice.json' 11 | 12 | chai.use(solidity) 13 | 14 | const overrides = { 15 | gasLimit: 9999999 16 | } 17 | 18 | describe('ExampleSwapToPrice', () => { 19 | const provider = new MockProvider({ 20 | hardfork: 'istanbul', 21 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 22 | gasLimit: 9999999 23 | }) 24 | const [wallet] = provider.getWallets() 25 | const loadFixture = createFixtureLoader(provider, [wallet]) 26 | 27 | let token0: Contract 28 | let token1: Contract 29 | let pair: Contract 30 | let swapToPriceExample: Contract 31 | let router: Contract 32 | beforeEach(async function() { 33 | const fixture = await loadFixture(v2Fixture) 34 | token0 = fixture.token0 35 | token1 = fixture.token1 36 | pair = fixture.pair 37 | router = fixture.router 38 | swapToPriceExample = await deployContract( 39 | wallet, 40 | ExampleSwapToPrice, 41 | [fixture.factoryV2.address, fixture.router.address], 42 | overrides 43 | ) 44 | }) 45 | 46 | beforeEach('set up price differential of 1:100', async () => { 47 | await token0.transfer(pair.address, expandTo18Decimals(10)) 48 | await token1.transfer(pair.address, expandTo18Decimals(1000)) 49 | await pair.sync(overrides) 50 | }) 51 | 52 | beforeEach('approve the swap contract to spend any amount of both tokens', async () => { 53 | await token0.approve(swapToPriceExample.address, MaxUint256) 54 | await token1.approve(swapToPriceExample.address, MaxUint256) 55 | }) 56 | 57 | it('correct router address', async () => { 58 | expect(await swapToPriceExample.router()).to.eq(router.address) 59 | }) 60 | 61 | describe('#swapToPrice', () => { 62 | it('requires non-zero true price inputs', async () => { 63 | await expect( 64 | swapToPriceExample.swapToPrice( 65 | token0.address, 66 | token1.address, 67 | 0, 68 | 0, 69 | MaxUint256, 70 | MaxUint256, 71 | wallet.address, 72 | MaxUint256 73 | ) 74 | ).to.be.revertedWith('ExampleSwapToPrice: ZERO_PRICE') 75 | await expect( 76 | swapToPriceExample.swapToPrice( 77 | token0.address, 78 | token1.address, 79 | 10, 80 | 0, 81 | MaxUint256, 82 | MaxUint256, 83 | wallet.address, 84 | MaxUint256 85 | ) 86 | ).to.be.revertedWith('ExampleSwapToPrice: ZERO_PRICE') 87 | await expect( 88 | swapToPriceExample.swapToPrice( 89 | token0.address, 90 | token1.address, 91 | 0, 92 | 10, 93 | MaxUint256, 94 | MaxUint256, 95 | wallet.address, 96 | MaxUint256 97 | ) 98 | ).to.be.revertedWith('ExampleSwapToPrice: ZERO_PRICE') 99 | }) 100 | 101 | it('requires non-zero max spend', async () => { 102 | await expect( 103 | swapToPriceExample.swapToPrice(token0.address, token1.address, 1, 100, 0, 0, wallet.address, MaxUint256) 104 | ).to.be.revertedWith('ExampleSwapToPrice: ZERO_SPEND') 105 | }) 106 | 107 | it('moves the price to 1:90', async () => { 108 | await expect( 109 | swapToPriceExample.swapToPrice( 110 | token0.address, 111 | token1.address, 112 | 1, 113 | 90, 114 | MaxUint256, 115 | MaxUint256, 116 | wallet.address, 117 | MaxUint256, 118 | overrides 119 | ) 120 | ) 121 | // (1e19 + 526682316179835569) : (1e21 - 49890467170695440744) ~= 1:90 122 | .to.emit(token0, 'Transfer') 123 | .withArgs(wallet.address, swapToPriceExample.address, '526682316179835569') 124 | .to.emit(token0, 'Approval') 125 | .withArgs(swapToPriceExample.address, router.address, '526682316179835569') 126 | .to.emit(token0, 'Transfer') 127 | .withArgs(swapToPriceExample.address, pair.address, '526682316179835569') 128 | .to.emit(token1, 'Transfer') 129 | .withArgs(pair.address, wallet.address, '49890467170695440744') 130 | }) 131 | 132 | it('moves the price to 1:110', async () => { 133 | await expect( 134 | swapToPriceExample.swapToPrice( 135 | token0.address, 136 | token1.address, 137 | 1, 138 | 110, 139 | MaxUint256, 140 | MaxUint256, 141 | wallet.address, 142 | MaxUint256, 143 | overrides 144 | ) 145 | ) 146 | // (1e21 + 47376582963642643588) : (1e19 - 451039908682851138) ~= 1:110 147 | .to.emit(token1, 'Transfer') 148 | .withArgs(wallet.address, swapToPriceExample.address, '47376582963642643588') 149 | .to.emit(token1, 'Approval') 150 | .withArgs(swapToPriceExample.address, router.address, '47376582963642643588') 151 | .to.emit(token1, 'Transfer') 152 | .withArgs(swapToPriceExample.address, pair.address, '47376582963642643588') 153 | .to.emit(token0, 'Transfer') 154 | .withArgs(pair.address, wallet.address, '451039908682851138') 155 | }) 156 | 157 | it('reverse token order', async () => { 158 | await expect( 159 | swapToPriceExample.swapToPrice( 160 | token1.address, 161 | token0.address, 162 | 110, 163 | 1, 164 | MaxUint256, 165 | MaxUint256, 166 | wallet.address, 167 | MaxUint256, 168 | overrides 169 | ) 170 | ) 171 | // (1e21 + 47376582963642643588) : (1e19 - 451039908682851138) ~= 1:110 172 | .to.emit(token1, 'Transfer') 173 | .withArgs(wallet.address, swapToPriceExample.address, '47376582963642643588') 174 | .to.emit(token1, 'Approval') 175 | .withArgs(swapToPriceExample.address, router.address, '47376582963642643588') 176 | .to.emit(token1, 'Transfer') 177 | .withArgs(swapToPriceExample.address, pair.address, '47376582963642643588') 178 | .to.emit(token0, 'Transfer') 179 | .withArgs(pair.address, wallet.address, '451039908682851138') 180 | }) 181 | 182 | it('swap gas cost', async () => { 183 | const tx = await swapToPriceExample.swapToPrice( 184 | token0.address, 185 | token1.address, 186 | 1, 187 | 110, 188 | MaxUint256, 189 | MaxUint256, 190 | wallet.address, 191 | MaxUint256, 192 | overrides 193 | ) 194 | const receipt = await tx.wait() 195 | expect(receipt.gasUsed).to.eq('115129') 196 | }).retries(2) // gas test is inconsistent 197 | }) 198 | }) 199 | -------------------------------------------------------------------------------- /test/UniswapV2Migrator.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { AddressZero, MaxUint256 } from 'ethers/constants' 4 | import { bigNumberify } from 'ethers/utils' 5 | import { solidity, MockProvider, createFixtureLoader } from 'ethereum-waffle' 6 | 7 | import { v2Fixture } from './shared/fixtures' 8 | import { expandTo18Decimals, MINIMUM_LIQUIDITY } from './shared/utilities' 9 | 10 | chai.use(solidity) 11 | 12 | const overrides = { 13 | gasLimit: 9999999 14 | } 15 | 16 | describe('UniswapV2Migrator', () => { 17 | const provider = new MockProvider({ 18 | hardfork: 'istanbul', 19 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 20 | gasLimit: 9999999 21 | }) 22 | const [wallet] = provider.getWallets() 23 | const loadFixture = createFixtureLoader(provider, [wallet]) 24 | 25 | let WETHPartner: Contract 26 | let WETHPair: Contract 27 | let router: Contract 28 | let migrator: Contract 29 | let WETHExchangeV1: Contract 30 | beforeEach(async function() { 31 | const fixture = await loadFixture(v2Fixture) 32 | WETHPartner = fixture.WETHPartner 33 | WETHPair = fixture.WETHPair 34 | router = fixture.router01 // we used router01 for this contract 35 | migrator = fixture.migrator 36 | WETHExchangeV1 = fixture.WETHExchangeV1 37 | }) 38 | 39 | it('migrate', async () => { 40 | const WETHPartnerAmount = expandTo18Decimals(1) 41 | const ETHAmount = expandTo18Decimals(4) 42 | await WETHPartner.approve(WETHExchangeV1.address, MaxUint256) 43 | await WETHExchangeV1.addLiquidity(bigNumberify(1), WETHPartnerAmount, MaxUint256, { 44 | ...overrides, 45 | value: ETHAmount 46 | }) 47 | await WETHExchangeV1.approve(migrator.address, MaxUint256) 48 | const expectedLiquidity = expandTo18Decimals(2) 49 | const WETHPairToken0 = await WETHPair.token0() 50 | await expect( 51 | migrator.migrate(WETHPartner.address, WETHPartnerAmount, ETHAmount, wallet.address, MaxUint256, overrides) 52 | ) 53 | .to.emit(WETHPair, 'Transfer') 54 | .withArgs(AddressZero, AddressZero, MINIMUM_LIQUIDITY) 55 | .to.emit(WETHPair, 'Transfer') 56 | .withArgs(AddressZero, wallet.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 57 | .to.emit(WETHPair, 'Sync') 58 | .withArgs( 59 | WETHPairToken0 === WETHPartner.address ? WETHPartnerAmount : ETHAmount, 60 | WETHPairToken0 === WETHPartner.address ? ETHAmount : WETHPartnerAmount 61 | ) 62 | .to.emit(WETHPair, 'Mint') 63 | .withArgs( 64 | router.address, 65 | WETHPairToken0 === WETHPartner.address ? WETHPartnerAmount : ETHAmount, 66 | WETHPairToken0 === WETHPartner.address ? ETHAmount : WETHPartnerAmount 67 | ) 68 | expect(await WETHPair.balanceOf(wallet.address)).to.eq(expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/UniswapV2Router01.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { AddressZero, Zero, MaxUint256 } from 'ethers/constants' 4 | import { BigNumber, bigNumberify } from 'ethers/utils' 5 | import { solidity, MockProvider, createFixtureLoader } from 'ethereum-waffle' 6 | import { ecsign } from 'ethereumjs-util' 7 | 8 | import { expandTo18Decimals, getApprovalDigest, mineBlock, MINIMUM_LIQUIDITY } from './shared/utilities' 9 | import { v2Fixture } from './shared/fixtures' 10 | 11 | chai.use(solidity) 12 | 13 | const overrides = { 14 | gasLimit: 9999999 15 | } 16 | 17 | enum RouterVersion { 18 | UniswapV2Router01 = 'UniswapV2Router01', 19 | UniswapV2Router02 = 'UniswapV2Router02' 20 | } 21 | 22 | describe('UniswapV2Router{01,02}', () => { 23 | for (const routerVersion of Object.keys(RouterVersion)) { 24 | const provider = new MockProvider({ 25 | hardfork: 'istanbul', 26 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 27 | gasLimit: 9999999 28 | }) 29 | const [wallet] = provider.getWallets() 30 | const loadFixture = createFixtureLoader(provider, [wallet]) 31 | 32 | let token0: Contract 33 | let token1: Contract 34 | let WETH: Contract 35 | let WETHPartner: Contract 36 | let factory: Contract 37 | let router: Contract 38 | let pair: Contract 39 | let WETHPair: Contract 40 | let routerEventEmitter: Contract 41 | beforeEach(async function() { 42 | const fixture = await loadFixture(v2Fixture) 43 | token0 = fixture.token0 44 | token1 = fixture.token1 45 | WETH = fixture.WETH 46 | WETHPartner = fixture.WETHPartner 47 | factory = fixture.factoryV2 48 | router = { 49 | [RouterVersion.UniswapV2Router01]: fixture.router01, 50 | [RouterVersion.UniswapV2Router02]: fixture.router02 51 | }[routerVersion as RouterVersion] 52 | pair = fixture.pair 53 | WETHPair = fixture.WETHPair 54 | routerEventEmitter = fixture.routerEventEmitter 55 | }) 56 | 57 | afterEach(async function() { 58 | expect(await provider.getBalance(router.address)).to.eq(Zero) 59 | }) 60 | 61 | describe(routerVersion, () => { 62 | it('factory, WETH', async () => { 63 | expect(await router.factory()).to.eq(factory.address) 64 | expect(await router.WETH()).to.eq(WETH.address) 65 | }) 66 | 67 | it('addLiquidity', async () => { 68 | const token0Amount = expandTo18Decimals(1) 69 | const token1Amount = expandTo18Decimals(4) 70 | 71 | const expectedLiquidity = expandTo18Decimals(2) 72 | await token0.approve(router.address, MaxUint256) 73 | await token1.approve(router.address, MaxUint256) 74 | await expect( 75 | router.addLiquidity( 76 | token0.address, 77 | token1.address, 78 | token0Amount, 79 | token1Amount, 80 | 0, 81 | 0, 82 | wallet.address, 83 | MaxUint256, 84 | overrides 85 | ) 86 | ) 87 | .to.emit(token0, 'Transfer') 88 | .withArgs(wallet.address, pair.address, token0Amount) 89 | .to.emit(token1, 'Transfer') 90 | .withArgs(wallet.address, pair.address, token1Amount) 91 | .to.emit(pair, 'Transfer') 92 | .withArgs(AddressZero, AddressZero, MINIMUM_LIQUIDITY) 93 | .to.emit(pair, 'Transfer') 94 | .withArgs(AddressZero, wallet.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 95 | .to.emit(pair, 'Sync') 96 | .withArgs(token0Amount, token1Amount) 97 | .to.emit(pair, 'Mint') 98 | .withArgs(router.address, token0Amount, token1Amount) 99 | 100 | expect(await pair.balanceOf(wallet.address)).to.eq(expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 101 | }) 102 | 103 | it('addLiquidityETH', async () => { 104 | const WETHPartnerAmount = expandTo18Decimals(1) 105 | const ETHAmount = expandTo18Decimals(4) 106 | 107 | const expectedLiquidity = expandTo18Decimals(2) 108 | const WETHPairToken0 = await WETHPair.token0() 109 | await WETHPartner.approve(router.address, MaxUint256) 110 | await expect( 111 | router.addLiquidityETH( 112 | WETHPartner.address, 113 | WETHPartnerAmount, 114 | WETHPartnerAmount, 115 | ETHAmount, 116 | wallet.address, 117 | MaxUint256, 118 | { ...overrides, value: ETHAmount } 119 | ) 120 | ) 121 | .to.emit(WETHPair, 'Transfer') 122 | .withArgs(AddressZero, AddressZero, MINIMUM_LIQUIDITY) 123 | .to.emit(WETHPair, 'Transfer') 124 | .withArgs(AddressZero, wallet.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 125 | .to.emit(WETHPair, 'Sync') 126 | .withArgs( 127 | WETHPairToken0 === WETHPartner.address ? WETHPartnerAmount : ETHAmount, 128 | WETHPairToken0 === WETHPartner.address ? ETHAmount : WETHPartnerAmount 129 | ) 130 | .to.emit(WETHPair, 'Mint') 131 | .withArgs( 132 | router.address, 133 | WETHPairToken0 === WETHPartner.address ? WETHPartnerAmount : ETHAmount, 134 | WETHPairToken0 === WETHPartner.address ? ETHAmount : WETHPartnerAmount 135 | ) 136 | 137 | expect(await WETHPair.balanceOf(wallet.address)).to.eq(expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 138 | }) 139 | 140 | async function addLiquidity(token0Amount: BigNumber, token1Amount: BigNumber) { 141 | await token0.transfer(pair.address, token0Amount) 142 | await token1.transfer(pair.address, token1Amount) 143 | await pair.mint(wallet.address, overrides) 144 | } 145 | it('removeLiquidity', async () => { 146 | const token0Amount = expandTo18Decimals(1) 147 | const token1Amount = expandTo18Decimals(4) 148 | await addLiquidity(token0Amount, token1Amount) 149 | 150 | const expectedLiquidity = expandTo18Decimals(2) 151 | await pair.approve(router.address, MaxUint256) 152 | await expect( 153 | router.removeLiquidity( 154 | token0.address, 155 | token1.address, 156 | expectedLiquidity.sub(MINIMUM_LIQUIDITY), 157 | 0, 158 | 0, 159 | wallet.address, 160 | MaxUint256, 161 | overrides 162 | ) 163 | ) 164 | .to.emit(pair, 'Transfer') 165 | .withArgs(wallet.address, pair.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 166 | .to.emit(pair, 'Transfer') 167 | .withArgs(pair.address, AddressZero, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 168 | .to.emit(token0, 'Transfer') 169 | .withArgs(pair.address, wallet.address, token0Amount.sub(500)) 170 | .to.emit(token1, 'Transfer') 171 | .withArgs(pair.address, wallet.address, token1Amount.sub(2000)) 172 | .to.emit(pair, 'Sync') 173 | .withArgs(500, 2000) 174 | .to.emit(pair, 'Burn') 175 | .withArgs(router.address, token0Amount.sub(500), token1Amount.sub(2000), wallet.address) 176 | 177 | expect(await pair.balanceOf(wallet.address)).to.eq(0) 178 | const totalSupplyToken0 = await token0.totalSupply() 179 | const totalSupplyToken1 = await token1.totalSupply() 180 | expect(await token0.balanceOf(wallet.address)).to.eq(totalSupplyToken0.sub(500)) 181 | expect(await token1.balanceOf(wallet.address)).to.eq(totalSupplyToken1.sub(2000)) 182 | }) 183 | 184 | it('removeLiquidityETH', async () => { 185 | const WETHPartnerAmount = expandTo18Decimals(1) 186 | const ETHAmount = expandTo18Decimals(4) 187 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 188 | await WETH.deposit({ value: ETHAmount }) 189 | await WETH.transfer(WETHPair.address, ETHAmount) 190 | await WETHPair.mint(wallet.address, overrides) 191 | 192 | const expectedLiquidity = expandTo18Decimals(2) 193 | const WETHPairToken0 = await WETHPair.token0() 194 | await WETHPair.approve(router.address, MaxUint256) 195 | await expect( 196 | router.removeLiquidityETH( 197 | WETHPartner.address, 198 | expectedLiquidity.sub(MINIMUM_LIQUIDITY), 199 | 0, 200 | 0, 201 | wallet.address, 202 | MaxUint256, 203 | overrides 204 | ) 205 | ) 206 | .to.emit(WETHPair, 'Transfer') 207 | .withArgs(wallet.address, WETHPair.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 208 | .to.emit(WETHPair, 'Transfer') 209 | .withArgs(WETHPair.address, AddressZero, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 210 | .to.emit(WETH, 'Transfer') 211 | .withArgs(WETHPair.address, router.address, ETHAmount.sub(2000)) 212 | .to.emit(WETHPartner, 'Transfer') 213 | .withArgs(WETHPair.address, router.address, WETHPartnerAmount.sub(500)) 214 | .to.emit(WETHPartner, 'Transfer') 215 | .withArgs(router.address, wallet.address, WETHPartnerAmount.sub(500)) 216 | .to.emit(WETHPair, 'Sync') 217 | .withArgs( 218 | WETHPairToken0 === WETHPartner.address ? 500 : 2000, 219 | WETHPairToken0 === WETHPartner.address ? 2000 : 500 220 | ) 221 | .to.emit(WETHPair, 'Burn') 222 | .withArgs( 223 | router.address, 224 | WETHPairToken0 === WETHPartner.address ? WETHPartnerAmount.sub(500) : ETHAmount.sub(2000), 225 | WETHPairToken0 === WETHPartner.address ? ETHAmount.sub(2000) : WETHPartnerAmount.sub(500), 226 | router.address 227 | ) 228 | 229 | expect(await WETHPair.balanceOf(wallet.address)).to.eq(0) 230 | const totalSupplyWETHPartner = await WETHPartner.totalSupply() 231 | const totalSupplyWETH = await WETH.totalSupply() 232 | expect(await WETHPartner.balanceOf(wallet.address)).to.eq(totalSupplyWETHPartner.sub(500)) 233 | expect(await WETH.balanceOf(wallet.address)).to.eq(totalSupplyWETH.sub(2000)) 234 | }) 235 | 236 | it('removeLiquidityWithPermit', async () => { 237 | const token0Amount = expandTo18Decimals(1) 238 | const token1Amount = expandTo18Decimals(4) 239 | await addLiquidity(token0Amount, token1Amount) 240 | 241 | const expectedLiquidity = expandTo18Decimals(2) 242 | 243 | const nonce = await pair.nonces(wallet.address) 244 | const digest = await getApprovalDigest( 245 | pair, 246 | { owner: wallet.address, spender: router.address, value: expectedLiquidity.sub(MINIMUM_LIQUIDITY) }, 247 | nonce, 248 | MaxUint256 249 | ) 250 | 251 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex')) 252 | 253 | await router.removeLiquidityWithPermit( 254 | token0.address, 255 | token1.address, 256 | expectedLiquidity.sub(MINIMUM_LIQUIDITY), 257 | 0, 258 | 0, 259 | wallet.address, 260 | MaxUint256, 261 | false, 262 | v, 263 | r, 264 | s, 265 | overrides 266 | ) 267 | }) 268 | 269 | it('removeLiquidityETHWithPermit', async () => { 270 | const WETHPartnerAmount = expandTo18Decimals(1) 271 | const ETHAmount = expandTo18Decimals(4) 272 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 273 | await WETH.deposit({ value: ETHAmount }) 274 | await WETH.transfer(WETHPair.address, ETHAmount) 275 | await WETHPair.mint(wallet.address, overrides) 276 | 277 | const expectedLiquidity = expandTo18Decimals(2) 278 | 279 | const nonce = await WETHPair.nonces(wallet.address) 280 | const digest = await getApprovalDigest( 281 | WETHPair, 282 | { owner: wallet.address, spender: router.address, value: expectedLiquidity.sub(MINIMUM_LIQUIDITY) }, 283 | nonce, 284 | MaxUint256 285 | ) 286 | 287 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex')) 288 | 289 | await router.removeLiquidityETHWithPermit( 290 | WETHPartner.address, 291 | expectedLiquidity.sub(MINIMUM_LIQUIDITY), 292 | 0, 293 | 0, 294 | wallet.address, 295 | MaxUint256, 296 | false, 297 | v, 298 | r, 299 | s, 300 | overrides 301 | ) 302 | }) 303 | 304 | describe('swapExactTokensForTokens', () => { 305 | const token0Amount = expandTo18Decimals(5) 306 | const token1Amount = expandTo18Decimals(10) 307 | const swapAmount = expandTo18Decimals(1) 308 | const expectedOutputAmount = bigNumberify('1662497915624478906') 309 | 310 | beforeEach(async () => { 311 | await addLiquidity(token0Amount, token1Amount) 312 | await token0.approve(router.address, MaxUint256) 313 | }) 314 | 315 | it('happy path', async () => { 316 | await expect( 317 | router.swapExactTokensForTokens( 318 | swapAmount, 319 | 0, 320 | [token0.address, token1.address], 321 | wallet.address, 322 | MaxUint256, 323 | overrides 324 | ) 325 | ) 326 | .to.emit(token0, 'Transfer') 327 | .withArgs(wallet.address, pair.address, swapAmount) 328 | .to.emit(token1, 'Transfer') 329 | .withArgs(pair.address, wallet.address, expectedOutputAmount) 330 | .to.emit(pair, 'Sync') 331 | .withArgs(token0Amount.add(swapAmount), token1Amount.sub(expectedOutputAmount)) 332 | .to.emit(pair, 'Swap') 333 | .withArgs(router.address, swapAmount, 0, 0, expectedOutputAmount, wallet.address) 334 | }) 335 | 336 | it('amounts', async () => { 337 | await token0.approve(routerEventEmitter.address, MaxUint256) 338 | await expect( 339 | routerEventEmitter.swapExactTokensForTokens( 340 | router.address, 341 | swapAmount, 342 | 0, 343 | [token0.address, token1.address], 344 | wallet.address, 345 | MaxUint256, 346 | overrides 347 | ) 348 | ) 349 | .to.emit(routerEventEmitter, 'Amounts') 350 | .withArgs([swapAmount, expectedOutputAmount]) 351 | }) 352 | 353 | it('gas', async () => { 354 | // ensure that setting price{0,1}CumulativeLast for the first time doesn't affect our gas math 355 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 356 | await pair.sync(overrides) 357 | 358 | await token0.approve(router.address, MaxUint256) 359 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 360 | const tx = await router.swapExactTokensForTokens( 361 | swapAmount, 362 | 0, 363 | [token0.address, token1.address], 364 | wallet.address, 365 | MaxUint256, 366 | overrides 367 | ) 368 | const receipt = await tx.wait() 369 | expect(receipt.gasUsed).to.eq( 370 | { 371 | [RouterVersion.UniswapV2Router01]: 101876, 372 | [RouterVersion.UniswapV2Router02]: 101898 373 | }[routerVersion as RouterVersion] 374 | ) 375 | }).retries(3) 376 | }) 377 | 378 | describe('swapTokensForExactTokens', () => { 379 | const token0Amount = expandTo18Decimals(5) 380 | const token1Amount = expandTo18Decimals(10) 381 | const expectedSwapAmount = bigNumberify('557227237267357629') 382 | const outputAmount = expandTo18Decimals(1) 383 | 384 | beforeEach(async () => { 385 | await addLiquidity(token0Amount, token1Amount) 386 | }) 387 | 388 | it('happy path', async () => { 389 | await token0.approve(router.address, MaxUint256) 390 | await expect( 391 | router.swapTokensForExactTokens( 392 | outputAmount, 393 | MaxUint256, 394 | [token0.address, token1.address], 395 | wallet.address, 396 | MaxUint256, 397 | overrides 398 | ) 399 | ) 400 | .to.emit(token0, 'Transfer') 401 | .withArgs(wallet.address, pair.address, expectedSwapAmount) 402 | .to.emit(token1, 'Transfer') 403 | .withArgs(pair.address, wallet.address, outputAmount) 404 | .to.emit(pair, 'Sync') 405 | .withArgs(token0Amount.add(expectedSwapAmount), token1Amount.sub(outputAmount)) 406 | .to.emit(pair, 'Swap') 407 | .withArgs(router.address, expectedSwapAmount, 0, 0, outputAmount, wallet.address) 408 | }) 409 | 410 | it('amounts', async () => { 411 | await token0.approve(routerEventEmitter.address, MaxUint256) 412 | await expect( 413 | routerEventEmitter.swapTokensForExactTokens( 414 | router.address, 415 | outputAmount, 416 | MaxUint256, 417 | [token0.address, token1.address], 418 | wallet.address, 419 | MaxUint256, 420 | overrides 421 | ) 422 | ) 423 | .to.emit(routerEventEmitter, 'Amounts') 424 | .withArgs([expectedSwapAmount, outputAmount]) 425 | }) 426 | }) 427 | 428 | describe('swapExactETHForTokens', () => { 429 | const WETHPartnerAmount = expandTo18Decimals(10) 430 | const ETHAmount = expandTo18Decimals(5) 431 | const swapAmount = expandTo18Decimals(1) 432 | const expectedOutputAmount = bigNumberify('1662497915624478906') 433 | 434 | beforeEach(async () => { 435 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 436 | await WETH.deposit({ value: ETHAmount }) 437 | await WETH.transfer(WETHPair.address, ETHAmount) 438 | await WETHPair.mint(wallet.address, overrides) 439 | 440 | await token0.approve(router.address, MaxUint256) 441 | }) 442 | 443 | it('happy path', async () => { 444 | const WETHPairToken0 = await WETHPair.token0() 445 | await expect( 446 | router.swapExactETHForTokens(0, [WETH.address, WETHPartner.address], wallet.address, MaxUint256, { 447 | ...overrides, 448 | value: swapAmount 449 | }) 450 | ) 451 | .to.emit(WETH, 'Transfer') 452 | .withArgs(router.address, WETHPair.address, swapAmount) 453 | .to.emit(WETHPartner, 'Transfer') 454 | .withArgs(WETHPair.address, wallet.address, expectedOutputAmount) 455 | .to.emit(WETHPair, 'Sync') 456 | .withArgs( 457 | WETHPairToken0 === WETHPartner.address 458 | ? WETHPartnerAmount.sub(expectedOutputAmount) 459 | : ETHAmount.add(swapAmount), 460 | WETHPairToken0 === WETHPartner.address 461 | ? ETHAmount.add(swapAmount) 462 | : WETHPartnerAmount.sub(expectedOutputAmount) 463 | ) 464 | .to.emit(WETHPair, 'Swap') 465 | .withArgs( 466 | router.address, 467 | WETHPairToken0 === WETHPartner.address ? 0 : swapAmount, 468 | WETHPairToken0 === WETHPartner.address ? swapAmount : 0, 469 | WETHPairToken0 === WETHPartner.address ? expectedOutputAmount : 0, 470 | WETHPairToken0 === WETHPartner.address ? 0 : expectedOutputAmount, 471 | wallet.address 472 | ) 473 | }) 474 | 475 | it('amounts', async () => { 476 | await expect( 477 | routerEventEmitter.swapExactETHForTokens( 478 | router.address, 479 | 0, 480 | [WETH.address, WETHPartner.address], 481 | wallet.address, 482 | MaxUint256, 483 | { 484 | ...overrides, 485 | value: swapAmount 486 | } 487 | ) 488 | ) 489 | .to.emit(routerEventEmitter, 'Amounts') 490 | .withArgs([swapAmount, expectedOutputAmount]) 491 | }) 492 | 493 | it('gas', async () => { 494 | const WETHPartnerAmount = expandTo18Decimals(10) 495 | const ETHAmount = expandTo18Decimals(5) 496 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 497 | await WETH.deposit({ value: ETHAmount }) 498 | await WETH.transfer(WETHPair.address, ETHAmount) 499 | await WETHPair.mint(wallet.address, overrides) 500 | 501 | // ensure that setting price{0,1}CumulativeLast for the first time doesn't affect our gas math 502 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 503 | await pair.sync(overrides) 504 | 505 | const swapAmount = expandTo18Decimals(1) 506 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 507 | const tx = await router.swapExactETHForTokens( 508 | 0, 509 | [WETH.address, WETHPartner.address], 510 | wallet.address, 511 | MaxUint256, 512 | { 513 | ...overrides, 514 | value: swapAmount 515 | } 516 | ) 517 | const receipt = await tx.wait() 518 | expect(receipt.gasUsed).to.eq( 519 | { 520 | [RouterVersion.UniswapV2Router01]: 138770, 521 | [RouterVersion.UniswapV2Router02]: 138770 522 | }[routerVersion as RouterVersion] 523 | ) 524 | }).retries(3) 525 | }) 526 | 527 | describe('swapTokensForExactETH', () => { 528 | const WETHPartnerAmount = expandTo18Decimals(5) 529 | const ETHAmount = expandTo18Decimals(10) 530 | const expectedSwapAmount = bigNumberify('557227237267357629') 531 | const outputAmount = expandTo18Decimals(1) 532 | 533 | beforeEach(async () => { 534 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 535 | await WETH.deposit({ value: ETHAmount }) 536 | await WETH.transfer(WETHPair.address, ETHAmount) 537 | await WETHPair.mint(wallet.address, overrides) 538 | }) 539 | 540 | it('happy path', async () => { 541 | await WETHPartner.approve(router.address, MaxUint256) 542 | const WETHPairToken0 = await WETHPair.token0() 543 | await expect( 544 | router.swapTokensForExactETH( 545 | outputAmount, 546 | MaxUint256, 547 | [WETHPartner.address, WETH.address], 548 | wallet.address, 549 | MaxUint256, 550 | overrides 551 | ) 552 | ) 553 | .to.emit(WETHPartner, 'Transfer') 554 | .withArgs(wallet.address, WETHPair.address, expectedSwapAmount) 555 | .to.emit(WETH, 'Transfer') 556 | .withArgs(WETHPair.address, router.address, outputAmount) 557 | .to.emit(WETHPair, 'Sync') 558 | .withArgs( 559 | WETHPairToken0 === WETHPartner.address 560 | ? WETHPartnerAmount.add(expectedSwapAmount) 561 | : ETHAmount.sub(outputAmount), 562 | WETHPairToken0 === WETHPartner.address 563 | ? ETHAmount.sub(outputAmount) 564 | : WETHPartnerAmount.add(expectedSwapAmount) 565 | ) 566 | .to.emit(WETHPair, 'Swap') 567 | .withArgs( 568 | router.address, 569 | WETHPairToken0 === WETHPartner.address ? expectedSwapAmount : 0, 570 | WETHPairToken0 === WETHPartner.address ? 0 : expectedSwapAmount, 571 | WETHPairToken0 === WETHPartner.address ? 0 : outputAmount, 572 | WETHPairToken0 === WETHPartner.address ? outputAmount : 0, 573 | router.address 574 | ) 575 | }) 576 | 577 | it('amounts', async () => { 578 | await WETHPartner.approve(routerEventEmitter.address, MaxUint256) 579 | await expect( 580 | routerEventEmitter.swapTokensForExactETH( 581 | router.address, 582 | outputAmount, 583 | MaxUint256, 584 | [WETHPartner.address, WETH.address], 585 | wallet.address, 586 | MaxUint256, 587 | overrides 588 | ) 589 | ) 590 | .to.emit(routerEventEmitter, 'Amounts') 591 | .withArgs([expectedSwapAmount, outputAmount]) 592 | }) 593 | }) 594 | 595 | describe('swapExactTokensForETH', () => { 596 | const WETHPartnerAmount = expandTo18Decimals(5) 597 | const ETHAmount = expandTo18Decimals(10) 598 | const swapAmount = expandTo18Decimals(1) 599 | const expectedOutputAmount = bigNumberify('1662497915624478906') 600 | 601 | beforeEach(async () => { 602 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 603 | await WETH.deposit({ value: ETHAmount }) 604 | await WETH.transfer(WETHPair.address, ETHAmount) 605 | await WETHPair.mint(wallet.address, overrides) 606 | }) 607 | 608 | it('happy path', async () => { 609 | await WETHPartner.approve(router.address, MaxUint256) 610 | const WETHPairToken0 = await WETHPair.token0() 611 | await expect( 612 | router.swapExactTokensForETH( 613 | swapAmount, 614 | 0, 615 | [WETHPartner.address, WETH.address], 616 | wallet.address, 617 | MaxUint256, 618 | overrides 619 | ) 620 | ) 621 | .to.emit(WETHPartner, 'Transfer') 622 | .withArgs(wallet.address, WETHPair.address, swapAmount) 623 | .to.emit(WETH, 'Transfer') 624 | .withArgs(WETHPair.address, router.address, expectedOutputAmount) 625 | .to.emit(WETHPair, 'Sync') 626 | .withArgs( 627 | WETHPairToken0 === WETHPartner.address 628 | ? WETHPartnerAmount.add(swapAmount) 629 | : ETHAmount.sub(expectedOutputAmount), 630 | WETHPairToken0 === WETHPartner.address 631 | ? ETHAmount.sub(expectedOutputAmount) 632 | : WETHPartnerAmount.add(swapAmount) 633 | ) 634 | .to.emit(WETHPair, 'Swap') 635 | .withArgs( 636 | router.address, 637 | WETHPairToken0 === WETHPartner.address ? swapAmount : 0, 638 | WETHPairToken0 === WETHPartner.address ? 0 : swapAmount, 639 | WETHPairToken0 === WETHPartner.address ? 0 : expectedOutputAmount, 640 | WETHPairToken0 === WETHPartner.address ? expectedOutputAmount : 0, 641 | router.address 642 | ) 643 | }) 644 | 645 | it('amounts', async () => { 646 | await WETHPartner.approve(routerEventEmitter.address, MaxUint256) 647 | await expect( 648 | routerEventEmitter.swapExactTokensForETH( 649 | router.address, 650 | swapAmount, 651 | 0, 652 | [WETHPartner.address, WETH.address], 653 | wallet.address, 654 | MaxUint256, 655 | overrides 656 | ) 657 | ) 658 | .to.emit(routerEventEmitter, 'Amounts') 659 | .withArgs([swapAmount, expectedOutputAmount]) 660 | }) 661 | }) 662 | 663 | describe('swapETHForExactTokens', () => { 664 | const WETHPartnerAmount = expandTo18Decimals(10) 665 | const ETHAmount = expandTo18Decimals(5) 666 | const expectedSwapAmount = bigNumberify('557227237267357629') 667 | const outputAmount = expandTo18Decimals(1) 668 | 669 | beforeEach(async () => { 670 | await WETHPartner.transfer(WETHPair.address, WETHPartnerAmount) 671 | await WETH.deposit({ value: ETHAmount }) 672 | await WETH.transfer(WETHPair.address, ETHAmount) 673 | await WETHPair.mint(wallet.address, overrides) 674 | }) 675 | 676 | it('happy path', async () => { 677 | const WETHPairToken0 = await WETHPair.token0() 678 | await expect( 679 | router.swapETHForExactTokens( 680 | outputAmount, 681 | [WETH.address, WETHPartner.address], 682 | wallet.address, 683 | MaxUint256, 684 | { 685 | ...overrides, 686 | value: expectedSwapAmount 687 | } 688 | ) 689 | ) 690 | .to.emit(WETH, 'Transfer') 691 | .withArgs(router.address, WETHPair.address, expectedSwapAmount) 692 | .to.emit(WETHPartner, 'Transfer') 693 | .withArgs(WETHPair.address, wallet.address, outputAmount) 694 | .to.emit(WETHPair, 'Sync') 695 | .withArgs( 696 | WETHPairToken0 === WETHPartner.address 697 | ? WETHPartnerAmount.sub(outputAmount) 698 | : ETHAmount.add(expectedSwapAmount), 699 | WETHPairToken0 === WETHPartner.address 700 | ? ETHAmount.add(expectedSwapAmount) 701 | : WETHPartnerAmount.sub(outputAmount) 702 | ) 703 | .to.emit(WETHPair, 'Swap') 704 | .withArgs( 705 | router.address, 706 | WETHPairToken0 === WETHPartner.address ? 0 : expectedSwapAmount, 707 | WETHPairToken0 === WETHPartner.address ? expectedSwapAmount : 0, 708 | WETHPairToken0 === WETHPartner.address ? outputAmount : 0, 709 | WETHPairToken0 === WETHPartner.address ? 0 : outputAmount, 710 | wallet.address 711 | ) 712 | }) 713 | 714 | it('amounts', async () => { 715 | await expect( 716 | routerEventEmitter.swapETHForExactTokens( 717 | router.address, 718 | outputAmount, 719 | [WETH.address, WETHPartner.address], 720 | wallet.address, 721 | MaxUint256, 722 | { 723 | ...overrides, 724 | value: expectedSwapAmount 725 | } 726 | ) 727 | ) 728 | .to.emit(routerEventEmitter, 'Amounts') 729 | .withArgs([expectedSwapAmount, outputAmount]) 730 | }) 731 | }) 732 | }) 733 | } 734 | }) 735 | -------------------------------------------------------------------------------- /test/UniswapV2Router02.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { solidity, MockProvider, createFixtureLoader, deployContract } from 'ethereum-waffle' 3 | import { Contract } from 'ethers' 4 | import { BigNumber, bigNumberify } from 'ethers/utils' 5 | import { MaxUint256 } from 'ethers/constants' 6 | import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json' 7 | 8 | import { v2Fixture } from './shared/fixtures' 9 | import { expandTo18Decimals, getApprovalDigest, MINIMUM_LIQUIDITY } from './shared/utilities' 10 | 11 | import DeflatingERC20 from '../build/DeflatingERC20.json' 12 | import { ecsign } from 'ethereumjs-util' 13 | 14 | chai.use(solidity) 15 | 16 | const overrides = { 17 | gasLimit: 9999999 18 | } 19 | 20 | describe('UniswapV2Router02', () => { 21 | const provider = new MockProvider({ 22 | hardfork: 'istanbul', 23 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 24 | gasLimit: 9999999 25 | }) 26 | const [wallet] = provider.getWallets() 27 | const loadFixture = createFixtureLoader(provider, [wallet]) 28 | 29 | let token0: Contract 30 | let token1: Contract 31 | let router: Contract 32 | beforeEach(async function() { 33 | const fixture = await loadFixture(v2Fixture) 34 | token0 = fixture.token0 35 | token1 = fixture.token1 36 | router = fixture.router02 37 | }) 38 | 39 | it('quote', async () => { 40 | expect(await router.quote(bigNumberify(1), bigNumberify(100), bigNumberify(200))).to.eq(bigNumberify(2)) 41 | expect(await router.quote(bigNumberify(2), bigNumberify(200), bigNumberify(100))).to.eq(bigNumberify(1)) 42 | await expect(router.quote(bigNumberify(0), bigNumberify(100), bigNumberify(200))).to.be.revertedWith( 43 | 'UniswapV2Library: INSUFFICIENT_AMOUNT' 44 | ) 45 | await expect(router.quote(bigNumberify(1), bigNumberify(0), bigNumberify(200))).to.be.revertedWith( 46 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 47 | ) 48 | await expect(router.quote(bigNumberify(1), bigNumberify(100), bigNumberify(0))).to.be.revertedWith( 49 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 50 | ) 51 | }) 52 | 53 | it('getAmountOut', async () => { 54 | expect(await router.getAmountOut(bigNumberify(2), bigNumberify(100), bigNumberify(100))).to.eq(bigNumberify(1)) 55 | await expect(router.getAmountOut(bigNumberify(0), bigNumberify(100), bigNumberify(100))).to.be.revertedWith( 56 | 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT' 57 | ) 58 | await expect(router.getAmountOut(bigNumberify(2), bigNumberify(0), bigNumberify(100))).to.be.revertedWith( 59 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 60 | ) 61 | await expect(router.getAmountOut(bigNumberify(2), bigNumberify(100), bigNumberify(0))).to.be.revertedWith( 62 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 63 | ) 64 | }) 65 | 66 | it('getAmountIn', async () => { 67 | expect(await router.getAmountIn(bigNumberify(1), bigNumberify(100), bigNumberify(100))).to.eq(bigNumberify(2)) 68 | await expect(router.getAmountIn(bigNumberify(0), bigNumberify(100), bigNumberify(100))).to.be.revertedWith( 69 | 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT' 70 | ) 71 | await expect(router.getAmountIn(bigNumberify(1), bigNumberify(0), bigNumberify(100))).to.be.revertedWith( 72 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 73 | ) 74 | await expect(router.getAmountIn(bigNumberify(1), bigNumberify(100), bigNumberify(0))).to.be.revertedWith( 75 | 'UniswapV2Library: INSUFFICIENT_LIQUIDITY' 76 | ) 77 | }) 78 | 79 | it('getAmountsOut', async () => { 80 | await token0.approve(router.address, MaxUint256) 81 | await token1.approve(router.address, MaxUint256) 82 | await router.addLiquidity( 83 | token0.address, 84 | token1.address, 85 | bigNumberify(10000), 86 | bigNumberify(10000), 87 | 0, 88 | 0, 89 | wallet.address, 90 | MaxUint256, 91 | overrides 92 | ) 93 | 94 | await expect(router.getAmountsOut(bigNumberify(2), [token0.address])).to.be.revertedWith( 95 | 'UniswapV2Library: INVALID_PATH' 96 | ) 97 | const path = [token0.address, token1.address] 98 | expect(await router.getAmountsOut(bigNumberify(2), path)).to.deep.eq([bigNumberify(2), bigNumberify(1)]) 99 | }) 100 | 101 | it('getAmountsIn', async () => { 102 | await token0.approve(router.address, MaxUint256) 103 | await token1.approve(router.address, MaxUint256) 104 | await router.addLiquidity( 105 | token0.address, 106 | token1.address, 107 | bigNumberify(10000), 108 | bigNumberify(10000), 109 | 0, 110 | 0, 111 | wallet.address, 112 | MaxUint256, 113 | overrides 114 | ) 115 | 116 | await expect(router.getAmountsIn(bigNumberify(1), [token0.address])).to.be.revertedWith( 117 | 'UniswapV2Library: INVALID_PATH' 118 | ) 119 | const path = [token0.address, token1.address] 120 | expect(await router.getAmountsIn(bigNumberify(1), path)).to.deep.eq([bigNumberify(2), bigNumberify(1)]) 121 | }) 122 | }) 123 | 124 | describe('fee-on-transfer tokens', () => { 125 | const provider = new MockProvider({ 126 | hardfork: 'istanbul', 127 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 128 | gasLimit: 9999999 129 | }) 130 | const [wallet] = provider.getWallets() 131 | const loadFixture = createFixtureLoader(provider, [wallet]) 132 | 133 | let DTT: Contract 134 | let WETH: Contract 135 | let router: Contract 136 | let pair: Contract 137 | beforeEach(async function() { 138 | const fixture = await loadFixture(v2Fixture) 139 | 140 | WETH = fixture.WETH 141 | router = fixture.router02 142 | 143 | DTT = await deployContract(wallet, DeflatingERC20, [expandTo18Decimals(10000)]) 144 | 145 | // make a DTT<>WETH pair 146 | await fixture.factoryV2.createPair(DTT.address, WETH.address) 147 | const pairAddress = await fixture.factoryV2.getPair(DTT.address, WETH.address) 148 | pair = new Contract(pairAddress, JSON.stringify(IUniswapV2Pair.abi), provider).connect(wallet) 149 | }) 150 | 151 | afterEach(async function() { 152 | expect(await provider.getBalance(router.address)).to.eq(0) 153 | }) 154 | 155 | async function addLiquidity(DTTAmount: BigNumber, WETHAmount: BigNumber) { 156 | await DTT.approve(router.address, MaxUint256) 157 | await router.addLiquidityETH(DTT.address, DTTAmount, DTTAmount, WETHAmount, wallet.address, MaxUint256, { 158 | ...overrides, 159 | value: WETHAmount 160 | }) 161 | } 162 | 163 | it('removeLiquidityETHSupportingFeeOnTransferTokens', async () => { 164 | const DTTAmount = expandTo18Decimals(1) 165 | const ETHAmount = expandTo18Decimals(4) 166 | await addLiquidity(DTTAmount, ETHAmount) 167 | 168 | const DTTInPair = await DTT.balanceOf(pair.address) 169 | const WETHInPair = await WETH.balanceOf(pair.address) 170 | const liquidity = await pair.balanceOf(wallet.address) 171 | const totalSupply = await pair.totalSupply() 172 | const NaiveDTTExpected = DTTInPair.mul(liquidity).div(totalSupply) 173 | const WETHExpected = WETHInPair.mul(liquidity).div(totalSupply) 174 | 175 | await pair.approve(router.address, MaxUint256) 176 | await router.removeLiquidityETHSupportingFeeOnTransferTokens( 177 | DTT.address, 178 | liquidity, 179 | NaiveDTTExpected, 180 | WETHExpected, 181 | wallet.address, 182 | MaxUint256, 183 | overrides 184 | ) 185 | }) 186 | 187 | it('removeLiquidityETHWithPermitSupportingFeeOnTransferTokens', async () => { 188 | const DTTAmount = expandTo18Decimals(1) 189 | .mul(100) 190 | .div(99) 191 | const ETHAmount = expandTo18Decimals(4) 192 | await addLiquidity(DTTAmount, ETHAmount) 193 | 194 | const expectedLiquidity = expandTo18Decimals(2) 195 | 196 | const nonce = await pair.nonces(wallet.address) 197 | const digest = await getApprovalDigest( 198 | pair, 199 | { owner: wallet.address, spender: router.address, value: expectedLiquidity.sub(MINIMUM_LIQUIDITY) }, 200 | nonce, 201 | MaxUint256 202 | ) 203 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex')) 204 | 205 | const DTTInPair = await DTT.balanceOf(pair.address) 206 | const WETHInPair = await WETH.balanceOf(pair.address) 207 | const liquidity = await pair.balanceOf(wallet.address) 208 | const totalSupply = await pair.totalSupply() 209 | const NaiveDTTExpected = DTTInPair.mul(liquidity).div(totalSupply) 210 | const WETHExpected = WETHInPair.mul(liquidity).div(totalSupply) 211 | 212 | await pair.approve(router.address, MaxUint256) 213 | await router.removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 214 | DTT.address, 215 | liquidity, 216 | NaiveDTTExpected, 217 | WETHExpected, 218 | wallet.address, 219 | MaxUint256, 220 | false, 221 | v, 222 | r, 223 | s, 224 | overrides 225 | ) 226 | }) 227 | 228 | describe('swapExactTokensForTokensSupportingFeeOnTransferTokens', () => { 229 | const DTTAmount = expandTo18Decimals(5) 230 | .mul(100) 231 | .div(99) 232 | const ETHAmount = expandTo18Decimals(10) 233 | const amountIn = expandTo18Decimals(1) 234 | 235 | beforeEach(async () => { 236 | await addLiquidity(DTTAmount, ETHAmount) 237 | }) 238 | 239 | it('DTT -> WETH', async () => { 240 | await DTT.approve(router.address, MaxUint256) 241 | 242 | await router.swapExactTokensForTokensSupportingFeeOnTransferTokens( 243 | amountIn, 244 | 0, 245 | [DTT.address, WETH.address], 246 | wallet.address, 247 | MaxUint256, 248 | overrides 249 | ) 250 | }) 251 | 252 | // WETH -> DTT 253 | it('WETH -> DTT', async () => { 254 | await WETH.deposit({ value: amountIn }) // mint WETH 255 | await WETH.approve(router.address, MaxUint256) 256 | 257 | await router.swapExactTokensForTokensSupportingFeeOnTransferTokens( 258 | amountIn, 259 | 0, 260 | [WETH.address, DTT.address], 261 | wallet.address, 262 | MaxUint256, 263 | overrides 264 | ) 265 | }) 266 | }) 267 | 268 | // ETH -> DTT 269 | it('swapExactETHForTokensSupportingFeeOnTransferTokens', async () => { 270 | const DTTAmount = expandTo18Decimals(10) 271 | .mul(100) 272 | .div(99) 273 | const ETHAmount = expandTo18Decimals(5) 274 | const swapAmount = expandTo18Decimals(1) 275 | await addLiquidity(DTTAmount, ETHAmount) 276 | 277 | await router.swapExactETHForTokensSupportingFeeOnTransferTokens( 278 | 0, 279 | [WETH.address, DTT.address], 280 | wallet.address, 281 | MaxUint256, 282 | { 283 | ...overrides, 284 | value: swapAmount 285 | } 286 | ) 287 | }) 288 | 289 | // DTT -> ETH 290 | it('swapExactTokensForETHSupportingFeeOnTransferTokens', async () => { 291 | const DTTAmount = expandTo18Decimals(5) 292 | .mul(100) 293 | .div(99) 294 | const ETHAmount = expandTo18Decimals(10) 295 | const swapAmount = expandTo18Decimals(1) 296 | 297 | await addLiquidity(DTTAmount, ETHAmount) 298 | await DTT.approve(router.address, MaxUint256) 299 | 300 | await router.swapExactTokensForETHSupportingFeeOnTransferTokens( 301 | swapAmount, 302 | 0, 303 | [DTT.address, WETH.address], 304 | wallet.address, 305 | MaxUint256, 306 | overrides 307 | ) 308 | }) 309 | }) 310 | 311 | describe('fee-on-transfer tokens: reloaded', () => { 312 | const provider = new MockProvider({ 313 | hardfork: 'istanbul', 314 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 315 | gasLimit: 9999999 316 | }) 317 | const [wallet] = provider.getWallets() 318 | const loadFixture = createFixtureLoader(provider, [wallet]) 319 | 320 | let DTT: Contract 321 | let DTT2: Contract 322 | let router: Contract 323 | beforeEach(async function() { 324 | const fixture = await loadFixture(v2Fixture) 325 | 326 | router = fixture.router02 327 | 328 | DTT = await deployContract(wallet, DeflatingERC20, [expandTo18Decimals(10000)]) 329 | DTT2 = await deployContract(wallet, DeflatingERC20, [expandTo18Decimals(10000)]) 330 | 331 | // make a DTT<>WETH pair 332 | await fixture.factoryV2.createPair(DTT.address, DTT2.address) 333 | const pairAddress = await fixture.factoryV2.getPair(DTT.address, DTT2.address) 334 | }) 335 | 336 | afterEach(async function() { 337 | expect(await provider.getBalance(router.address)).to.eq(0) 338 | }) 339 | 340 | async function addLiquidity(DTTAmount: BigNumber, DTT2Amount: BigNumber) { 341 | await DTT.approve(router.address, MaxUint256) 342 | await DTT2.approve(router.address, MaxUint256) 343 | await router.addLiquidity( 344 | DTT.address, 345 | DTT2.address, 346 | DTTAmount, 347 | DTT2Amount, 348 | DTTAmount, 349 | DTT2Amount, 350 | wallet.address, 351 | MaxUint256, 352 | overrides 353 | ) 354 | } 355 | 356 | describe('swapExactTokensForTokensSupportingFeeOnTransferTokens', () => { 357 | const DTTAmount = expandTo18Decimals(5) 358 | .mul(100) 359 | .div(99) 360 | const DTT2Amount = expandTo18Decimals(5) 361 | const amountIn = expandTo18Decimals(1) 362 | 363 | beforeEach(async () => { 364 | await addLiquidity(DTTAmount, DTT2Amount) 365 | }) 366 | 367 | it('DTT -> DTT2', async () => { 368 | await DTT.approve(router.address, MaxUint256) 369 | 370 | await router.swapExactTokensForTokensSupportingFeeOnTransferTokens( 371 | amountIn, 372 | 0, 373 | [DTT.address, DTT2.address], 374 | wallet.address, 375 | MaxUint256, 376 | overrides 377 | ) 378 | }) 379 | }) 380 | }) 381 | -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, Contract } from 'ethers' 2 | import { Web3Provider } from 'ethers/providers' 3 | import { deployContract } from 'ethereum-waffle' 4 | 5 | import { expandTo18Decimals } from './utilities' 6 | 7 | import UniswapV2Factory from '@uniswap/v2-core/build/UniswapV2Factory.json' 8 | import IUniswapV2Pair from '@uniswap/v2-core/build/IUniswapV2Pair.json' 9 | 10 | import ERC20 from '../../build/ERC20.json' 11 | import WETH9 from '../../build/WETH9.json' 12 | import UniswapV1Exchange from '../../build/UniswapV1Exchange.json' 13 | import UniswapV1Factory from '../../build/UniswapV1Factory.json' 14 | import UniswapV2Router01 from '../../build/UniswapV2Router01.json' 15 | import UniswapV2Migrator from '../../build/UniswapV2Migrator.json' 16 | import UniswapV2Router02 from '../../build/UniswapV2Router02.json' 17 | import RouterEventEmitter from '../../build/RouterEventEmitter.json' 18 | 19 | const overrides = { 20 | gasLimit: 9999999 21 | } 22 | 23 | interface V2Fixture { 24 | token0: Contract 25 | token1: Contract 26 | WETH: Contract 27 | WETHPartner: Contract 28 | factoryV1: Contract 29 | factoryV2: Contract 30 | router01: Contract 31 | router02: Contract 32 | routerEventEmitter: Contract 33 | router: Contract 34 | migrator: Contract 35 | WETHExchangeV1: Contract 36 | pair: Contract 37 | WETHPair: Contract 38 | } 39 | 40 | export async function v2Fixture(provider: Web3Provider, [wallet]: Wallet[]): Promise { 41 | // deploy tokens 42 | const tokenA = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)]) 43 | const tokenB = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)]) 44 | const WETH = await deployContract(wallet, WETH9) 45 | const WETHPartner = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)]) 46 | 47 | // deploy V1 48 | const factoryV1 = await deployContract(wallet, UniswapV1Factory, []) 49 | await factoryV1.initializeFactory((await deployContract(wallet, UniswapV1Exchange, [])).address) 50 | 51 | // deploy V2 52 | const factoryV2 = await deployContract(wallet, UniswapV2Factory, [wallet.address]) 53 | 54 | // deploy routers 55 | const router01 = await deployContract(wallet, UniswapV2Router01, [factoryV2.address, WETH.address], overrides) 56 | const router02 = await deployContract(wallet, UniswapV2Router02, [factoryV2.address, WETH.address], overrides) 57 | 58 | // event emitter for testing 59 | const routerEventEmitter = await deployContract(wallet, RouterEventEmitter, []) 60 | 61 | // deploy migrator 62 | const migrator = await deployContract(wallet, UniswapV2Migrator, [factoryV1.address, router01.address], overrides) 63 | 64 | // initialize V1 65 | await factoryV1.createExchange(WETHPartner.address, overrides) 66 | const WETHExchangeV1Address = await factoryV1.getExchange(WETHPartner.address) 67 | const WETHExchangeV1 = new Contract(WETHExchangeV1Address, JSON.stringify(UniswapV1Exchange.abi), provider).connect( 68 | wallet 69 | ) 70 | 71 | // initialize V2 72 | await factoryV2.createPair(tokenA.address, tokenB.address) 73 | const pairAddress = await factoryV2.getPair(tokenA.address, tokenB.address) 74 | const pair = new Contract(pairAddress, JSON.stringify(IUniswapV2Pair.abi), provider).connect(wallet) 75 | 76 | const token0Address = await pair.token0() 77 | const token0 = tokenA.address === token0Address ? tokenA : tokenB 78 | const token1 = tokenA.address === token0Address ? tokenB : tokenA 79 | 80 | await factoryV2.createPair(WETH.address, WETHPartner.address) 81 | const WETHPairAddress = await factoryV2.getPair(WETH.address, WETHPartner.address) 82 | const WETHPair = new Contract(WETHPairAddress, JSON.stringify(IUniswapV2Pair.abi), provider).connect(wallet) 83 | 84 | return { 85 | token0, 86 | token1, 87 | WETH, 88 | WETHPartner, 89 | factoryV1, 90 | factoryV2, 91 | router01, 92 | router02, 93 | router: router02, // the default router, 01 had a minor bug 94 | routerEventEmitter, 95 | migrator, 96 | WETHExchangeV1, 97 | pair, 98 | WETHPair 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Web3Provider } from 'ethers/providers' 3 | import { BigNumber, bigNumberify, keccak256, defaultAbiCoder, toUtf8Bytes, solidityPack } from 'ethers/utils' 4 | 5 | export const MINIMUM_LIQUIDITY = bigNumberify(10).pow(3) 6 | 7 | const PERMIT_TYPEHASH = keccak256( 8 | toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') 9 | ) 10 | 11 | export function expandTo18Decimals(n: number): BigNumber { 12 | return bigNumberify(n).mul(bigNumberify(10).pow(18)) 13 | } 14 | 15 | function getDomainSeparator(name: string, tokenAddress: string) { 16 | return keccak256( 17 | defaultAbiCoder.encode( 18 | ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], 19 | [ 20 | keccak256(toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), 21 | keccak256(toUtf8Bytes(name)), 22 | keccak256(toUtf8Bytes('1')), 23 | 1, 24 | tokenAddress 25 | ] 26 | ) 27 | ) 28 | } 29 | 30 | export async function getApprovalDigest( 31 | token: Contract, 32 | approve: { 33 | owner: string 34 | spender: string 35 | value: BigNumber 36 | }, 37 | nonce: BigNumber, 38 | deadline: BigNumber 39 | ): Promise { 40 | const name = await token.name() 41 | const DOMAIN_SEPARATOR = getDomainSeparator(name, token.address) 42 | return keccak256( 43 | solidityPack( 44 | ['bytes1', 'bytes1', 'bytes32', 'bytes32'], 45 | [ 46 | '0x19', 47 | '0x01', 48 | DOMAIN_SEPARATOR, 49 | keccak256( 50 | defaultAbiCoder.encode( 51 | ['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], 52 | [PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline] 53 | ) 54 | ) 55 | ] 56 | ) 57 | ) 58 | } 59 | 60 | export async function mineBlock(provider: Web3Provider, timestamp: number): Promise { 61 | await new Promise(async (resolve, reject) => { 62 | ;(provider._web3Provider.sendAsync as any)( 63 | { jsonrpc: '2.0', method: 'evm_mine', params: [timestamp] }, 64 | (error: any, result: any): void => { 65 | if (error) { 66 | reject(error) 67 | } else { 68 | resolve(result) 69 | } 70 | } 71 | ) 72 | }) 73 | } 74 | 75 | export function encodePrice(reserve0: BigNumber, reserve1: BigNumber) { 76 | return [reserve1.mul(bigNumberify(2).pow(112)).div(reserve0), reserve0.mul(bigNumberify(2).pow(112)).div(reserve1)] 77 | } 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------