├── .prettierignore ├── .yarnrc ├── .gitattributes ├── .prettierrc ├── .gitignore ├── .solhint.json ├── test ├── __snapshots__ │ ├── Path.spec.ts.snap │ ├── SwapRouter.spec.ts.snap │ ├── ImmutableState.spec.ts.snap │ ├── Multicall.spec.ts.snap │ ├── PoolAddress.spec.ts.snap │ ├── MixedRouteQuoterV1.spec.ts.snap │ ├── SwapRouter.gas.spec.ts.snap │ └── QuoterV2.spec.ts.snap ├── shared │ ├── ticks.ts │ ├── expect.ts │ ├── expandTo18Decimals.ts │ ├── encodePriceSqrt.ts │ ├── constants.ts │ ├── computePoolAddress.ts │ ├── snapshotGasCost.ts │ ├── completeFixture.ts │ ├── path.ts │ ├── externalFixtures.ts │ └── quoter.ts ├── PeripheryPaymentsExtended.spec.ts ├── ImmutableState.spec.ts ├── MulticallExtended.spec.ts ├── TokenValidator.spec.ts ├── Quoter.spec.ts ├── MixedRouteQuoterV1.integ.ts ├── contracts │ └── WETH9.json ├── PoolTicksCounter.spec.ts └── ApproveAndCall.spec.ts ├── contracts ├── lens │ ├── README.md │ ├── TokenValidator.sol │ ├── Quoter.sol │ ├── MixedRouteQuoterV1.sol │ └── QuoterV2.sol ├── interfaces │ ├── IWETH.sol │ ├── IImmutableState.sol │ ├── ISwapRouter02.sol │ ├── IPeripheryPaymentsWithFeeExtended.sol │ ├── IOracleSlippage.sol │ ├── IMulticallExtended.sol │ ├── IV2SwapRouter.sol │ ├── IPeripheryPaymentsExtended.sol │ ├── IApproveAndCall.sol │ ├── ITokenValidator.sol │ ├── IQuoter.sol │ ├── IV3SwapRouter.sol │ ├── IMixedRouteQuoterV1.sol │ └── IQuoterV2.sol ├── test │ ├── ImmutableStateTest.sol │ ├── TestERC20.sol │ ├── PoolTicksCounterTest.sol │ ├── MockTimeSwapRouter.sol │ ├── TestMulticallExtended.sol │ ├── OracleSlippageTest.sol │ ├── TestUniswapV3Callee.sol │ └── MockObservations.sol ├── base │ ├── PeripheryValidationExtended.sol │ ├── ImmutableState.sol │ ├── MulticallExtended.sol │ ├── PeripheryPaymentsWithFeeExtended.sol │ ├── PeripheryPaymentsExtended.sol │ ├── ApproveAndCall.sol │ └── OracleSlippage.sol ├── libraries │ ├── Constants.sol │ ├── UniswapV2Library.sol │ └── PoolTicksCounter.sol ├── SwapRouter02.sol ├── V2SwapRouter.sol └── V3SwapRouter.sol ├── tsconfig.json ├── .github └── workflows │ ├── lint.yml │ ├── tests.yml │ └── release.yml ├── hardhat.config.ts ├── package.json ├── README.md └── bug-bounty.md /.prettierignore: -------------------------------------------------------------------------------- 1 | .github -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | ignore-scripts true 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | cache/ 3 | crytic-export/ 4 | node_modules/ 5 | typechain/ 6 | .env 7 | .vscode -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier"], 3 | "rules": { 4 | "prettier/prettier": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/__snapshots__/Path.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Path gas cost 1`] = `451`; 4 | -------------------------------------------------------------------------------- /test/__snapshots__/SwapRouter.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SwapRouter bytecode size 1`] = `24563`; 4 | -------------------------------------------------------------------------------- /test/__snapshots__/ImmutableState.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ImmutableState bytecode size 1`] = `193`; 4 | -------------------------------------------------------------------------------- /contracts/lens/README.md: -------------------------------------------------------------------------------- 1 | # lens 2 | 3 | These contracts are not designed to be called on-chain. They simplify 4 | fetching on-chain data from off-chain. 5 | -------------------------------------------------------------------------------- /test/shared/ticks.ts: -------------------------------------------------------------------------------- 1 | export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing 2 | export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing 3 | -------------------------------------------------------------------------------- /test/__snapshots__/Multicall.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Multicall gas cost of pay w/ multicall 1`] = `45928`; 4 | 5 | exports[`Multicall gas cost of pay w/o multicall 1`] = `43364`; 6 | -------------------------------------------------------------------------------- /test/shared/expect.ts: -------------------------------------------------------------------------------- 1 | import { expect, use } from 'chai' 2 | import { solidity } from 'ethereum-waffle' 3 | import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot' 4 | 5 | use(solidity) 6 | use(jestSnapshotPlugin()) 7 | 8 | export { expect } 9 | -------------------------------------------------------------------------------- /test/__snapshots__/PoolAddress.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PoolAddress #computeAddress gas cost 1`] = `642`; 4 | 5 | exports[`PoolAddress #computeAddress matches example from core repo 1`] = `"0x03D8bab195A5BC23d249693F53dfA0e358F2650D"`; 6 | -------------------------------------------------------------------------------- /contracts/interfaces/IWETH.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | 7 | function transfer(address to, uint256 value) external returns (bool); 8 | 9 | function withdraw(uint256) external; 10 | } 11 | -------------------------------------------------------------------------------- /contracts/test/ImmutableStateTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | 4 | import '../base/ImmutableState.sol'; 5 | 6 | contract ImmutableStateTest is ImmutableState { 7 | constructor(address _factoryV2, address _positionManager) ImmutableState(_factoryV2, _positionManager) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/shared/expandTo18Decimals.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | export function expandTo18Decimals(n: number): BigNumber { 4 | return BigNumber.from(n).mul(BigNumber.from(10).pow(18)) 5 | } 6 | 7 | export function expandToNDecimals(n: number, d: number): BigNumber { 8 | return BigNumber.from(n).mul(BigNumber.from(10).pow(d)) 9 | } 10 | -------------------------------------------------------------------------------- /contracts/test/TestERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import '@openzeppelin/contracts/drafts/ERC20Permit.sol'; 5 | 6 | contract TestERC20 is ERC20Permit { 7 | constructor(uint256 amountToMint) ERC20('Test ERC20', 'TEST') ERC20Permit('Test ERC20') { 8 | _mint(msg.sender, amountToMint); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "outDir": "dist", 9 | "typeRoots": ["./typechain", "./node_modules/@types"], 10 | "types": ["@nomiclabs/hardhat-ethers", "@nomiclabs/hardhat-waffle"] 11 | }, 12 | "include": ["./test"], 13 | "files": ["./hardhat.config.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /contracts/base/PeripheryValidationExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | 4 | import '@uniswap/v3-periphery/contracts/base/PeripheryValidation.sol'; 5 | 6 | abstract contract PeripheryValidationExtended is PeripheryValidation { 7 | modifier checkPreviousBlockhash(bytes32 previousBlockhash) { 8 | require(blockhash(block.number - 1) == previousBlockhash, 'Blockhash'); 9 | _; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IImmutableState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Immutable state 5 | /// @notice Functions that return immutable state of the router 6 | interface IImmutableState { 7 | /// @return Returns the address of the Uniswap V2 factory 8 | function factoryV2() external view returns (address); 9 | 10 | /// @return Returns the address of Uniswap V3 NFT position manager 11 | function positionManager() external view returns (address); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/ISwapRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/interfaces/ISelfPermit.sol'; 6 | 7 | import './IV2SwapRouter.sol'; 8 | import './IV3SwapRouter.sol'; 9 | import './IApproveAndCall.sol'; 10 | import './IMulticallExtended.sol'; 11 | 12 | /// @title Router token swapping functionality 13 | interface ISwapRouter02 is IV2SwapRouter, IV3SwapRouter, IApproveAndCall, IMulticallExtended, ISelfPermit { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /test/shared/encodePriceSqrt.ts: -------------------------------------------------------------------------------- 1 | import bn from 'bignumber.js' 2 | import { BigNumber, BigNumberish } from 'ethers' 3 | 4 | bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 }) 5 | 6 | // returns the sqrt price as a 64x96 7 | export function encodePriceSqrt(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber { 8 | return BigNumber.from( 9 | new bn(reserve1.toString()) 10 | .div(reserve0.toString()) 11 | .sqrt() 12 | .multipliedBy(new bn(2).pow(96)) 13 | .integerValue(3) 14 | .toString() 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /contracts/test/PoolTicksCounterTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 3 | 4 | pragma solidity >=0.6.0; 5 | 6 | import '../libraries/PoolTicksCounter.sol'; 7 | 8 | contract PoolTicksCounterTest { 9 | using PoolTicksCounter for IUniswapV3Pool; 10 | 11 | function countInitializedTicksCrossed( 12 | IUniswapV3Pool pool, 13 | int24 tickBefore, 14 | int24 tickAfter 15 | ) external view returns (uint32 initializedTicksCrossed) { 16 | return pool.countInitializedTicksCrossed(tickBefore, tickAfter); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'ethers' 2 | 3 | export const MaxUint128 = BigNumber.from(2).pow(128).sub(1) 4 | 5 | export enum FeeAmount { 6 | LOW = 500, 7 | MEDIUM = 3000, 8 | HIGH = 10000, 9 | } 10 | 11 | export const V2_FEE_PLACEHOLDER = 8388608 // 1 << 23 12 | 13 | export const TICK_SPACINGS: { [amount in FeeAmount]: number } = { 14 | [FeeAmount.LOW]: 10, 15 | [FeeAmount.MEDIUM]: 60, 16 | [FeeAmount.HIGH]: 200, 17 | } 18 | 19 | export const CONTRACT_BALANCE = 0 20 | export const MSG_SENDER = '0x0000000000000000000000000000000000000001' 21 | export const ADDRESS_THIS = '0x0000000000000000000000000000000000000002' 22 | -------------------------------------------------------------------------------- /contracts/base/ImmutableState.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | 4 | import '../interfaces/IImmutableState.sol'; 5 | 6 | /// @title Immutable state 7 | /// @notice Immutable state used by the swap router 8 | abstract contract ImmutableState is IImmutableState { 9 | /// @inheritdoc IImmutableState 10 | address public immutable override factoryV2; 11 | /// @inheritdoc IImmutableState 12 | address public immutable override positionManager; 13 | 14 | constructor(address _factoryV2, address _positionManager) { 15 | factoryV2 = _factoryV2; 16 | positionManager = _positionManager; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/libraries/Constants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | 4 | /// @title Constant state 5 | /// @notice Constant state used by the swap router 6 | library Constants { 7 | /// @dev Used for identifying cases when this contract's balance of a token is to be used 8 | uint256 internal constant CONTRACT_BALANCE = 0; 9 | 10 | /// @dev Used as a flag for identifying msg.sender, saves gas by sending more 0 bytes 11 | address internal constant MSG_SENDER = address(1); 12 | 13 | /// @dev Used as a flag for identifying address(this), saves gas by sending more 0 bytes 14 | address internal constant ADDRESS_THIS = address(2); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/test/MockTimeSwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '../SwapRouter02.sol'; 6 | 7 | contract MockTimeSwapRouter02 is SwapRouter02 { 8 | uint256 time; 9 | 10 | constructor( 11 | address _factoryV2, 12 | address factoryV3, 13 | address _positionManager, 14 | address _WETH9 15 | ) SwapRouter02(_factoryV2, factoryV3, _positionManager, _WETH9) {} 16 | 17 | function _blockTimestamp() internal view override returns (uint256) { 18 | return time; 19 | } 20 | 21 | function setTime(uint256 _time) external { 22 | time = _time; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/test/TestMulticallExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '../base/MulticallExtended.sol'; 6 | 7 | contract TestMulticallExtended is MulticallExtended { 8 | uint256 time; 9 | 10 | function _blockTimestamp() internal view override returns (uint256) { 11 | return time; 12 | } 13 | 14 | function setTime(uint256 _time) external { 15 | time = _time; 16 | } 17 | 18 | struct Tuple { 19 | uint256 a; 20 | uint256 b; 21 | } 22 | 23 | function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) { 24 | tuple = Tuple({b: a, a: b}); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/SwapRouter02.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/base/SelfPermit.sol'; 6 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 7 | 8 | import './interfaces/ISwapRouter02.sol'; 9 | import './V2SwapRouter.sol'; 10 | import './V3SwapRouter.sol'; 11 | import './base/ApproveAndCall.sol'; 12 | import './base/MulticallExtended.sol'; 13 | 14 | /// @title Uniswap V2 and V3 Swap Router 15 | contract SwapRouter02 is ISwapRouter02, V2SwapRouter, V3SwapRouter, ApproveAndCall, MulticallExtended, SelfPermit { 16 | constructor( 17 | address _factoryV2, 18 | address factoryV3, 19 | address _positionManager, 20 | address _WETH9 21 | ) ImmutableState(_factoryV2, _positionManager) PeripheryImmutableState(factoryV3, _WETH9) {} 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run-linters: 11 | name: Run linters 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 12.x 22 | registry-url: https://registry.npmjs.org 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Run linters 28 | uses: wearerequired/lint-action@a8497ddb33fb1205941fd40452ca9fff07e0770d 29 | with: 30 | github_token: ${{ secrets.github_token }} 31 | prettier: true 32 | auto_fix: true 33 | prettier_extensions: 'css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml,yml,sol' 34 | -------------------------------------------------------------------------------- /test/shared/computePoolAddress.ts: -------------------------------------------------------------------------------- 1 | import { bytecode } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json' 2 | import { utils } from 'ethers' 3 | 4 | export const POOL_BYTECODE_HASH = utils.keccak256(bytecode) 5 | 6 | export function computePoolAddress(factoryAddress: string, [tokenA, tokenB]: [string, string], fee: number): string { 7 | const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA] 8 | const constructorArgumentsEncoded = utils.defaultAbiCoder.encode( 9 | ['address', 'address', 'uint24'], 10 | [token0, token1, fee] 11 | ) 12 | const create2Inputs = [ 13 | '0xff', 14 | factoryAddress, 15 | // salt 16 | utils.keccak256(constructorArgumentsEncoded), 17 | // init code hash 18 | POOL_BYTECODE_HASH, 19 | ] 20 | const sanitizedInputs = `0x${create2Inputs.map((i) => i.slice(2)).join('')}` 21 | return utils.getAddress(`0x${utils.keccak256(sanitizedInputs).slice(-40)}`) 22 | } 23 | -------------------------------------------------------------------------------- /test/shared/snapshotGasCost.ts: -------------------------------------------------------------------------------- 1 | import { TransactionReceipt, TransactionResponse } from '@ethersproject/abstract-provider' 2 | import { expect } from './expect' 3 | import { Contract, BigNumber, ContractTransaction } from 'ethers' 4 | 5 | export default async function snapshotGasCost( 6 | x: 7 | | TransactionResponse 8 | | Promise 9 | | ContractTransaction 10 | | Promise 11 | | TransactionReceipt 12 | | Promise 13 | | BigNumber 14 | | Contract 15 | | Promise 16 | ): Promise { 17 | const resolved = await x 18 | if ('deployTransaction' in resolved) { 19 | const receipt = await resolved.deployTransaction.wait() 20 | expect(receipt.gasUsed.toNumber()).toMatchSnapshot() 21 | } else if ('wait' in resolved) { 22 | const waited = await resolved.wait() 23 | expect(waited.gasUsed.toNumber()).toMatchSnapshot() 24 | } else if (BigNumber.isBigNumber(resolved)) { 25 | expect(resolved.toNumber()).toMatchSnapshot() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit-tests: 11 | name: Unit Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 12.x 19 | registry-url: https://registry.npmjs.org 20 | 21 | - id: yarn-cache 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - uses: actions/cache@v1 25 | with: 26 | path: ${{ steps.yarn-cache.outputs.dir }} 27 | key: yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | yarn- 30 | 31 | - name: Install dependencies 32 | run: yarn install --frozen-lockfile 33 | 34 | # This is required separately from yarn test because it generates the typechain definitions 35 | - name: Compile 36 | run: yarn compile 37 | 38 | - name: Run unit tests 39 | run: yarn test 40 | env: # Or as an environment variable 41 | ARCHIVE_RPC_URL: ${{ secrets.ARCHIVE_RPC_URL }} 42 | -------------------------------------------------------------------------------- /contracts/base/MulticallExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/base/Multicall.sol'; 6 | 7 | import '../interfaces/IMulticallExtended.sol'; 8 | import '../base/PeripheryValidationExtended.sol'; 9 | 10 | /// @title Multicall 11 | /// @notice Enables calling multiple methods in a single call to the contract 12 | abstract contract MulticallExtended is IMulticallExtended, Multicall, PeripheryValidationExtended { 13 | /// @inheritdoc IMulticallExtended 14 | function multicall(uint256 deadline, bytes[] calldata data) 15 | external 16 | payable 17 | override 18 | checkDeadline(deadline) 19 | returns (bytes[] memory) 20 | { 21 | return multicall(data); 22 | } 23 | 24 | /// @inheritdoc IMulticallExtended 25 | function multicall(bytes32 previousBlockhash, bytes[] calldata data) 26 | external 27 | payable 28 | override 29 | checkPreviousBlockhash(previousBlockhash) 30 | returns (bytes[] memory) 31 | { 32 | return multicall(data); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/base/PeripheryPaymentsWithFeeExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | 4 | import '@uniswap/v3-periphery/contracts/base/PeripheryPaymentsWithFee.sol'; 5 | 6 | import '../interfaces/IPeripheryPaymentsWithFeeExtended.sol'; 7 | import './PeripheryPaymentsExtended.sol'; 8 | 9 | abstract contract PeripheryPaymentsWithFeeExtended is 10 | IPeripheryPaymentsWithFeeExtended, 11 | PeripheryPaymentsExtended, 12 | PeripheryPaymentsWithFee 13 | { 14 | /// @inheritdoc IPeripheryPaymentsWithFeeExtended 15 | function unwrapWETH9WithFee( 16 | uint256 amountMinimum, 17 | uint256 feeBips, 18 | address feeRecipient 19 | ) external payable override { 20 | unwrapWETH9WithFee(amountMinimum, msg.sender, feeBips, feeRecipient); 21 | } 22 | 23 | /// @inheritdoc IPeripheryPaymentsWithFeeExtended 24 | function sweepTokenWithFee( 25 | address token, 26 | uint256 amountMinimum, 27 | uint256 feeBips, 28 | address feeRecipient 29 | ) external payable override { 30 | sweepTokenWithFee(token, amountMinimum, msg.sender, feeBips, feeRecipient); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/base/PeripheryPaymentsExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | 4 | import '@uniswap/v3-periphery/contracts/base/PeripheryPayments.sol'; 5 | import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol'; 6 | 7 | import '../interfaces/IPeripheryPaymentsExtended.sol'; 8 | 9 | abstract contract PeripheryPaymentsExtended is IPeripheryPaymentsExtended, PeripheryPayments { 10 | /// @inheritdoc IPeripheryPaymentsExtended 11 | function unwrapWETH9(uint256 amountMinimum) external payable override { 12 | unwrapWETH9(amountMinimum, msg.sender); 13 | } 14 | 15 | /// @inheritdoc IPeripheryPaymentsExtended 16 | function wrapETH(uint256 value) external payable override { 17 | IWETH9(WETH9).deposit{value: value}(); 18 | } 19 | 20 | /// @inheritdoc IPeripheryPaymentsExtended 21 | function sweepToken(address token, uint256 amountMinimum) external payable override { 22 | sweepToken(token, amountMinimum, msg.sender); 23 | } 24 | 25 | /// @inheritdoc IPeripheryPaymentsExtended 26 | function pull(address token, uint256 value) external payable override { 27 | TransferHelper.safeTransferFrom(token, msg.sender, address(this), value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/shared/completeFixture.ts: -------------------------------------------------------------------------------- 1 | import { Fixture } from 'ethereum-waffle' 2 | import { ethers, waffle } from 'hardhat' 3 | import { v3RouterFixture } from './externalFixtures' 4 | import { constants, Contract } from 'ethers' 5 | import { IWETH9, MockTimeSwapRouter02, TestERC20 } from '../../typechain' 6 | 7 | const completeFixture: Fixture<{ 8 | weth9: IWETH9 9 | factoryV2: Contract 10 | factory: Contract 11 | router: MockTimeSwapRouter02 12 | nft: Contract 13 | tokens: [TestERC20, TestERC20, TestERC20] 14 | }> = async ([wallet], provider) => { 15 | const { weth9, factoryV2, factory, nft, router } = await v3RouterFixture([wallet], provider) 16 | 17 | const tokenFactory = await ethers.getContractFactory('TestERC20') 18 | const tokens: [TestERC20, TestERC20, TestERC20] = [ 19 | (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20, // do not use maxu256 to avoid overflowing 20 | (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20, 21 | (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20, 22 | ] 23 | 24 | tokens.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1)) 25 | 26 | return { 27 | weth9, 28 | factoryV2, 29 | factory, 30 | router, 31 | tokens, 32 | nft, 33 | } 34 | } 35 | 36 | export default completeFixture 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: workflow_dispatch # TODO set this on a deploy schedule 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | environment: 9 | name: release 10 | runs-on: 11 | group: npm-deploy 12 | steps: 13 | - name: Load Secrets 14 | uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0 15 | with: 16 | # Export loaded secrets as environment variables 17 | export-env: true 18 | env: 19 | OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} 20 | NPM_TOKEN: op://npm-deploy/npm-runner-token/secret 21 | 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | submodules: "true" 26 | fetch-depth: 2 27 | 28 | - name: Setup Node 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 18 32 | registry-url: 'https://registry.npmjs.org' 33 | # Defaults to the user or organization that owns the workflow file 34 | scope: '@uniswap' 35 | 36 | - name: Setup CI 37 | run: npm ci 38 | 39 | - name: Build 40 | run: npm run build 41 | 42 | - name: Publish 43 | run: npm publish 44 | env: 45 | NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /contracts/interfaces/IPeripheryPaymentsWithFeeExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | 4 | import '@uniswap/v3-periphery/contracts/interfaces/IPeripheryPaymentsWithFee.sol'; 5 | 6 | import './IPeripheryPaymentsExtended.sol'; 7 | 8 | /// @title Periphery Payments With Fee Extended 9 | /// @notice Functions to ease deposits and withdrawals of ETH 10 | interface IPeripheryPaymentsWithFeeExtended is IPeripheryPaymentsExtended, IPeripheryPaymentsWithFee { 11 | /// @notice Unwraps the contract's WETH9 balance and sends it to msg.sender as ETH, with a percentage between 12 | /// 0 (exclusive), and 1 (inclusive) going to feeRecipient 13 | /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. 14 | function unwrapWETH9WithFee( 15 | uint256 amountMinimum, 16 | uint256 feeBips, 17 | address feeRecipient 18 | ) external payable; 19 | 20 | /// @notice Transfers the full amount of a token held by this contract to msg.sender, with a percentage between 21 | /// 0 (exclusive) and 1 (inclusive) going to feeRecipient 22 | /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users 23 | function sweepTokenWithFee( 24 | address token, 25 | uint256 amountMinimum, 26 | uint256 feeBips, 27 | address feeRecipient 28 | ) external payable; 29 | } 30 | -------------------------------------------------------------------------------- /test/__snapshots__/MixedRouteQuoterV1.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 -> 1 1`] = `281505`; 4 | 5 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 0 tick, starting tick initialized 1`] = `123786`; 6 | 7 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 0 tick, starting tick not initialized 1`] = `104827`; 8 | 9 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 1 tick 1`] = `149094`; 10 | 11 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 2 tick 1`] = `186691`; 12 | 13 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 2 tick where after is initialized 1`] = `149132`; 14 | 15 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 0 tick, starting tick initialized 1`] = `97643`; 16 | 17 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 0 tick, starting tick not initialized 1`] = `97643`; 18 | 19 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 2 1`] = `179503`; 20 | 21 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 2 where tick after is initialized 1`] = `179511`; 22 | 23 | exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 1 1`] = `97318`; 24 | 25 | exports[`MixedRouteQuoterV1 quotes #quoteExactInputSingle V3 0 -> 2 1`] = `186673`; 26 | 27 | exports[`MixedRouteQuoterV1 quotes #quoteExactInputSingle V3 2 -> 0 1`] = `179475`; 28 | -------------------------------------------------------------------------------- /contracts/interfaces/IOracleSlippage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title OracleSlippage interface 6 | /// @notice Enables slippage checks against oracle prices 7 | interface IOracleSlippage { 8 | /// @notice Ensures that the current (synthetic) tick over the path is no worse than 9 | /// `maximumTickDivergence` ticks away from the average as of `secondsAgo` 10 | /// @param path The path to fetch prices over 11 | /// @param maximumTickDivergence The maximum number of ticks that the price can degrade by 12 | /// @param secondsAgo The number of seconds ago to compute oracle prices against 13 | function checkOracleSlippage( 14 | bytes memory path, 15 | uint24 maximumTickDivergence, 16 | uint32 secondsAgo 17 | ) external view; 18 | 19 | /// @notice Ensures that the weighted average current (synthetic) tick over the path is no 20 | /// worse than `maximumTickDivergence` ticks away from the average as of `secondsAgo` 21 | /// @param paths The paths to fetch prices over 22 | /// @param amounts The weights for each entry in `paths` 23 | /// @param maximumTickDivergence The maximum number of ticks that the price can degrade by 24 | /// @param secondsAgo The number of seconds ago to compute oracle prices against 25 | function checkOracleSlippage( 26 | bytes[] memory paths, 27 | uint128[] memory amounts, 28 | uint24 maximumTickDivergence, 29 | uint32 secondsAgo 30 | ) external view; 31 | } 32 | -------------------------------------------------------------------------------- /test/__snapshots__/SwapRouter.gas.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SwapRouter gas tests #exactInput 0 -> 1 -> 2 1`] = `175302`; 4 | 5 | exports[`SwapRouter gas tests #exactInput 0 -> 1 1`] = `110495`; 6 | 7 | exports[`SwapRouter gas tests #exactInput 0 -> 1 minimal 1`] = `98059`; 8 | 9 | exports[`SwapRouter gas tests #exactInput 0 -> WETH9 1`] = `127401`; 10 | 11 | exports[`SwapRouter gas tests #exactInput 2 trades (directly to sender) 1`] = `178981`; 12 | 13 | exports[`SwapRouter gas tests #exactInput 2 trades (via router) 1`] = `188487`; 14 | 15 | exports[`SwapRouter gas tests #exactInput 3 trades (directly to sender) 1`] = `257705`; 16 | 17 | exports[`SwapRouter gas tests #exactInput WETH9 -> 0 1`] = `108832`; 18 | 19 | exports[`SwapRouter gas tests #exactInputSingle 0 -> 1 1`] = `109826`; 20 | 21 | exports[`SwapRouter gas tests #exactInputSingle 0 -> WETH9 1`] = `126732`; 22 | 23 | exports[`SwapRouter gas tests #exactInputSingle WETH9 -> 0 1`] = `108163`; 24 | 25 | exports[`SwapRouter gas tests #exactOutput 0 -> 1 -> 2 1`] = `169268`; 26 | 27 | exports[`SwapRouter gas tests #exactOutput 0 -> 1 1`] = `111692`; 28 | 29 | exports[`SwapRouter gas tests #exactOutput 0 -> WETH9 1`] = `128610`; 30 | 31 | exports[`SwapRouter gas tests #exactOutput WETH9 -> 0 1`] = `119706`; 32 | 33 | exports[`SwapRouter gas tests #exactOutputSingle 0 -> 1 1`] = `111826`; 34 | 35 | exports[`SwapRouter gas tests #exactOutputSingle 0 -> WETH9 1`] = `128744`; 36 | 37 | exports[`SwapRouter gas tests #exactOutputSingle WETH9 -> 0 1`] = `112627`; 38 | -------------------------------------------------------------------------------- /contracts/interfaces/IMulticallExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/interfaces/IMulticall.sol'; 6 | 7 | /// @title MulticallExtended interface 8 | /// @notice Enables calling multiple methods in a single call to the contract with optional validation 9 | interface IMulticallExtended is IMulticall { 10 | /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed 11 | /// @dev The `msg.value` should not be trusted for any method callable from multicall. 12 | /// @param deadline The time by which this function must be called before failing 13 | /// @param data The encoded function data for each of the calls to make to this contract 14 | /// @return results The results from each of the calls passed in via data 15 | function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results); 16 | 17 | /// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed 18 | /// @dev The `msg.value` should not be trusted for any method callable from multicall. 19 | /// @param previousBlockhash The expected parent blockHash 20 | /// @param data The encoded function data for each of the calls to make to this contract 21 | /// @return results The results from each of the calls passed in via data 22 | function multicall(bytes32 previousBlockhash, bytes[] calldata data) 23 | external 24 | payable 25 | returns (bytes[] memory results); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/IV2SwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title Router token swapping functionality 6 | /// @notice Functions for swapping tokens via Uniswap V2 7 | interface IV2SwapRouter { 8 | /// @notice Swaps `amountIn` of one token for as much as possible of another token 9 | /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, 10 | /// and swap the entire amount, enabling contracts to send tokens before calling this function. 11 | /// @param amountIn The amount of token to swap 12 | /// @param amountOutMin The minimum amount of output that must be received 13 | /// @param path The ordered list of tokens to swap through 14 | /// @param to The recipient address 15 | /// @return amountOut The amount of the received token 16 | function swapExactTokensForTokens( 17 | uint256 amountIn, 18 | uint256 amountOutMin, 19 | address[] calldata path, 20 | address to 21 | ) external payable returns (uint256 amountOut); 22 | 23 | /// @notice Swaps as little as possible of one token for an exact amount of another token 24 | /// @param amountOut The amount of token to swap for 25 | /// @param amountInMax The maximum amount of input that the caller will pay 26 | /// @param path The ordered list of tokens to swap through 27 | /// @param to The recipient address 28 | /// @return amountIn The amount of token to pay 29 | function swapTokensForExactTokens( 30 | uint256 amountOut, 31 | uint256 amountInMax, 32 | address[] calldata path, 33 | address to 34 | ) external payable returns (uint256 amountIn); 35 | } 36 | -------------------------------------------------------------------------------- /contracts/interfaces/IPeripheryPaymentsExtended.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | 4 | import '@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol'; 5 | 6 | /// @title Periphery Payments Extended 7 | /// @notice Functions to ease deposits and withdrawals of ETH and tokens 8 | interface IPeripheryPaymentsExtended is IPeripheryPayments { 9 | /// @notice Unwraps the contract's WETH9 balance and sends it to msg.sender as ETH. 10 | /// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users. 11 | /// @param amountMinimum The minimum amount of WETH9 to unwrap 12 | function unwrapWETH9(uint256 amountMinimum) external payable; 13 | 14 | /// @notice Wraps the contract's ETH balance into WETH9 15 | /// @dev The resulting WETH9 is custodied by the router, thus will require further distribution 16 | /// @param value The amount of ETH to wrap 17 | function wrapETH(uint256 value) external payable; 18 | 19 | /// @notice Transfers the full amount of a token held by this contract to msg.sender 20 | /// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users 21 | /// @param token The contract address of the token which will be transferred to msg.sender 22 | /// @param amountMinimum The minimum amount of token required for a transfer 23 | function sweepToken(address token, uint256 amountMinimum) external payable; 24 | 25 | /// @notice Transfers the specified amount of a token from the msg.sender to address(this) 26 | /// @param token The token to pull 27 | /// @param value The amount to pay 28 | function pull(address token, uint256 value) external payable; 29 | } 30 | -------------------------------------------------------------------------------- /test/PeripheryPaymentsExtended.spec.ts: -------------------------------------------------------------------------------- 1 | import { Fixture } from 'ethereum-waffle' 2 | import { constants, Contract, ContractTransaction, Wallet } from 'ethers' 3 | import { waffle, ethers } from 'hardhat' 4 | import { IWETH9, MockTimeSwapRouter02 } from '../typechain' 5 | import completeFixture from './shared/completeFixture' 6 | import { expect } from './shared/expect' 7 | 8 | describe('PeripheryPaymentsExtended', function () { 9 | let wallet: Wallet 10 | 11 | const routerFixture: Fixture<{ 12 | weth9: IWETH9 13 | router: MockTimeSwapRouter02 14 | }> = async (wallets, provider) => { 15 | const { weth9, router } = await completeFixture(wallets, provider) 16 | 17 | return { 18 | weth9, 19 | router, 20 | } 21 | } 22 | 23 | let router: MockTimeSwapRouter02 24 | let weth9: IWETH9 25 | 26 | let loadFixture: ReturnType 27 | 28 | before('create fixture loader', async () => { 29 | ;[wallet] = await (ethers as any).getSigners() 30 | loadFixture = waffle.createFixtureLoader([wallet]) 31 | }) 32 | 33 | beforeEach('load fixture', async () => { 34 | ;({ weth9, router } = await loadFixture(routerFixture)) 35 | }) 36 | 37 | describe('wrapETH', () => { 38 | it('increases router WETH9 balance by value amount', async () => { 39 | const value = ethers.utils.parseEther('1') 40 | 41 | const weth9BalancePrev = await weth9.balanceOf(router.address) 42 | await router.wrapETH(value, { value }) 43 | const weth9BalanceCurrent = await weth9.balanceOf(router.address) 44 | 45 | expect(weth9BalanceCurrent.sub(weth9BalancePrev)).to.equal(value) 46 | expect(await weth9.balanceOf(wallet.address)).to.equal('0') 47 | expect(await router.provider.getBalance(router.address)).to.equal('0') 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import '@nomiclabs/hardhat-ethers' 2 | import '@nomiclabs/hardhat-etherscan' 3 | import '@nomiclabs/hardhat-waffle' 4 | import 'hardhat-typechain' 5 | import 'hardhat-watcher' 6 | import 'dotenv/config' 7 | 8 | const DEFAULT_COMPILER_SETTINGS = { 9 | version: '0.7.6', 10 | settings: { 11 | evmVersion: 'istanbul', 12 | optimizer: { 13 | enabled: true, 14 | runs: 1_000_000, 15 | }, 16 | metadata: { 17 | bytecodeHash: 'none', 18 | }, 19 | }, 20 | } 21 | 22 | export default { 23 | networks: { 24 | hardhat: { 25 | allowUnlimitedContractSize: false, 26 | }, 27 | mainnet: { 28 | url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, 29 | }, 30 | ropsten: { 31 | url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`, 32 | }, 33 | rinkeby: { 34 | url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, 35 | }, 36 | goerli: { 37 | url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, 38 | }, 39 | kovan: { 40 | url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`, 41 | }, 42 | arbitrumRinkeby: { 43 | url: `https://rinkeby.arbitrum.io/rpc`, 44 | }, 45 | arbitrum: { 46 | url: `https://arb1.arbitrum.io/rpc`, 47 | }, 48 | optimismKovan: { 49 | url: `https://kovan.optimism.io`, 50 | }, 51 | optimism: { 52 | url: `https://mainnet.optimism.io`, 53 | }, 54 | }, 55 | etherscan: { 56 | // Your API key for Etherscan 57 | // Obtain one at https://etherscan.io/ 58 | apiKey: process.env.ETHERSCAN_API_KEY, 59 | }, 60 | solidity: { 61 | compilers: [DEFAULT_COMPILER_SETTINGS], 62 | }, 63 | watcher: { 64 | test: { 65 | tasks: [{ command: 'test', params: { testFiles: ['{path}'] } }], 66 | files: ['./test/**/*'], 67 | verbose: true, 68 | }, 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /test/ImmutableState.spec.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { waffle, ethers } from 'hardhat' 3 | 4 | import { Fixture } from 'ethereum-waffle' 5 | import { ImmutableStateTest } from '../typechain' 6 | import { expect } from './shared/expect' 7 | import completeFixture from './shared/completeFixture' 8 | import { v2FactoryFixture } from './shared/externalFixtures' 9 | 10 | describe('ImmutableState', () => { 11 | const fixture: Fixture<{ 12 | factoryV2: Contract 13 | nft: Contract 14 | state: ImmutableStateTest 15 | }> = async (wallets, provider) => { 16 | const { factory: factoryV2 } = await v2FactoryFixture(wallets, provider) 17 | const { nft } = await completeFixture(wallets, provider) 18 | 19 | const stateFactory = await ethers.getContractFactory('ImmutableStateTest') 20 | const state = (await stateFactory.deploy(factoryV2.address, nft.address)) as ImmutableStateTest 21 | 22 | return { 23 | nft, 24 | factoryV2, 25 | state, 26 | } 27 | } 28 | 29 | let factoryV2: Contract 30 | let nft: Contract 31 | let state: ImmutableStateTest 32 | 33 | let loadFixture: ReturnType 34 | 35 | before('create fixture loader', async () => { 36 | loadFixture = waffle.createFixtureLoader(await (ethers as any).getSigners()) 37 | }) 38 | 39 | beforeEach('load fixture', async () => { 40 | ;({ factoryV2, nft, state } = await loadFixture(fixture)) 41 | }) 42 | 43 | it('bytecode size', async () => { 44 | expect(((await state.provider.getCode(state.address)).length - 2) / 2).to.matchSnapshot() 45 | }) 46 | 47 | describe('#factoryV2', () => { 48 | it('points to v2 core factory', async () => { 49 | expect(await state.factoryV2()).to.eq(factoryV2.address) 50 | }) 51 | }) 52 | 53 | describe('#positionManager', () => { 54 | it('points to NFT', async () => { 55 | expect(await state.positionManager()).to.eq(nft.address) 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/MulticallExtended.spec.ts: -------------------------------------------------------------------------------- 1 | import { constants } from 'ethers' 2 | import { ethers } from 'hardhat' 3 | import { TestMulticallExtended } from '../typechain/TestMulticallExtended' 4 | import { expect } from './shared/expect' 5 | 6 | describe('MulticallExtended', async () => { 7 | let multicall: TestMulticallExtended 8 | 9 | beforeEach('create multicall', async () => { 10 | const multicallTestFactory = await ethers.getContractFactory('TestMulticallExtended') 11 | multicall = (await multicallTestFactory.deploy()) as TestMulticallExtended 12 | }) 13 | 14 | it('fails deadline check', async () => { 15 | await multicall.setTime(1) 16 | await expect( 17 | multicall['multicall(uint256,bytes[])'](0, [ 18 | multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']), 19 | ]) 20 | ).to.be.revertedWith('Transaction too old') 21 | }) 22 | 23 | it('passes deadline check', async () => { 24 | const [data] = await multicall.callStatic['multicall(uint256,bytes[])'](0, [ 25 | multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']), 26 | ]) 27 | const { 28 | tuple: { a, b }, 29 | } = multicall.interface.decodeFunctionResult('functionThatReturnsTuple', data) 30 | expect(b).to.eq(1) 31 | expect(a).to.eq(2) 32 | }) 33 | 34 | it('fails previousBlockhash check', async () => { 35 | await expect( 36 | multicall['multicall(bytes32,bytes[])'](constants.HashZero, [ 37 | multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']), 38 | ]) 39 | ).to.be.revertedWith('Blockhash') 40 | }) 41 | 42 | it('passes previousBlockhash check', async () => { 43 | const block = await ethers.provider.getBlock('latest') 44 | await expect( 45 | multicall['multicall(bytes32,bytes[])'](block.hash, [ 46 | multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']), 47 | ]) 48 | ).to.not.be.reverted 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/shared/path.ts: -------------------------------------------------------------------------------- 1 | import { utils } from 'ethers' 2 | import { FeeAmount } from './constants' 3 | 4 | const ADDR_SIZE = 20 5 | const FEE_SIZE = 3 6 | const OFFSET = ADDR_SIZE + FEE_SIZE 7 | const DATA_SIZE = OFFSET + ADDR_SIZE 8 | 9 | export function encodePath(path: string[], fees: FeeAmount[]): string { 10 | if (path.length != fees.length + 1) { 11 | throw new Error('path/fee lengths do not match') 12 | } 13 | 14 | let encoded = '0x' 15 | for (let i = 0; i < fees.length; i++) { 16 | // 20 byte encoding of the address 17 | encoded += path[i].slice(2) 18 | // 3 byte encoding of the fee 19 | encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0') 20 | } 21 | // encode the final token 22 | encoded += path[path.length - 1].slice(2) 23 | 24 | return encoded.toLowerCase() 25 | } 26 | 27 | function decodeOne(tokenFeeToken: Buffer): [[string, string], number] { 28 | // reads the first 20 bytes for the token address 29 | const tokenABuf = tokenFeeToken.slice(0, ADDR_SIZE) 30 | const tokenA = utils.getAddress('0x' + tokenABuf.toString('hex')) 31 | 32 | // reads the next 2 bytes for the fee 33 | const feeBuf = tokenFeeToken.slice(ADDR_SIZE, OFFSET) 34 | const fee = feeBuf.readUIntBE(0, FEE_SIZE) 35 | 36 | // reads the next 20 bytes for the token address 37 | const tokenBBuf = tokenFeeToken.slice(OFFSET, DATA_SIZE) 38 | const tokenB = utils.getAddress('0x' + tokenBBuf.toString('hex')) 39 | 40 | return [[tokenA, tokenB], fee] 41 | } 42 | 43 | export function decodePath(path: string): [string[], number[]] { 44 | let data = Buffer.from(path.slice(2), 'hex') 45 | 46 | let tokens: string[] = [] 47 | let fees: number[] = [] 48 | let i = 0 49 | let finalToken: string = '' 50 | while (data.length >= DATA_SIZE) { 51 | const [[tokenA, tokenB], fee] = decodeOne(data) 52 | finalToken = tokenB 53 | tokens = [...tokens, tokenA] 54 | fees = [...fees, fee] 55 | data = data.slice((i + 1) * OFFSET) 56 | i += 1 57 | } 58 | tokens = [...tokens, finalToken] 59 | 60 | return [tokens, fees] 61 | } 62 | -------------------------------------------------------------------------------- /contracts/test/OracleSlippageTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '../base/OracleSlippage.sol'; 6 | 7 | contract OracleSlippageTest is OracleSlippage { 8 | mapping(address => mapping(address => mapping(uint24 => IUniswapV3Pool))) private pools; 9 | uint256 internal time; 10 | 11 | constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {} 12 | 13 | function setTime(uint256 _time) external { 14 | time = _time; 15 | } 16 | 17 | function _blockTimestamp() internal view override returns (uint256) { 18 | return time; 19 | } 20 | 21 | function registerPool( 22 | IUniswapV3Pool pool, 23 | address tokenIn, 24 | address tokenOut, 25 | uint24 fee 26 | ) external { 27 | pools[tokenIn][tokenOut][fee] = pool; 28 | pools[tokenOut][tokenIn][fee] = pool; 29 | } 30 | 31 | function getPoolAddress( 32 | address tokenA, 33 | address tokenB, 34 | uint24 fee 35 | ) internal view override returns (IUniswapV3Pool pool) { 36 | pool = pools[tokenA][tokenB][fee]; 37 | } 38 | 39 | function testGetBlockStartingAndCurrentTick(IUniswapV3Pool pool) 40 | external 41 | view 42 | returns (int24 blockStartingTick, int24 currentTick) 43 | { 44 | return getBlockStartingAndCurrentTick(pool); 45 | } 46 | 47 | function testGetSyntheticTicks(bytes memory path, uint32 secondsAgo) 48 | external 49 | view 50 | returns (int256 syntheticAverageTick, int256 syntheticCurrentTick) 51 | { 52 | return getSyntheticTicks(path, secondsAgo); 53 | } 54 | 55 | function testGetSyntheticTicks( 56 | bytes[] memory paths, 57 | uint128[] memory amounts, 58 | uint32 secondsAgo 59 | ) external view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) { 60 | return getSyntheticTicks(paths, amounts, secondsAgo); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/swap-router-contracts", 3 | "description": "Smart contracts for swapping on Uniswap V2 and V3", 4 | "license": "GPL-2.0-or-later", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "version": "1.3.1", 9 | "homepage": "https://uniswap.org", 10 | "keywords": [ 11 | "uniswap", 12 | "v2", 13 | "v3" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Uniswap/swap-router-contracts" 18 | }, 19 | "files": [ 20 | "contracts/base", 21 | "contracts/interfaces", 22 | "contracts/libraries", 23 | "artifacts/contracts/**/*.json", 24 | "!artifacts/contracts/**/*.dbg.json", 25 | "!artifacts/contracts/test/**/*", 26 | "!artifacts/contracts/base/**/*" 27 | ], 28 | "engines": { 29 | "node": ">=10" 30 | }, 31 | "dependencies": { 32 | "@openzeppelin/contracts": "3.4.2-solc-0.7", 33 | "@uniswap/v2-core": "^1.0.1", 34 | "@uniswap/v3-core": "^1.0.0", 35 | "@uniswap/v3-periphery": "^1.4.4", 36 | "dotenv": "^14.2.0", 37 | "hardhat-watcher": "^2.1.1" 38 | }, 39 | "devDependencies": { 40 | "@nomiclabs/hardhat-ethers": "^2.0.2", 41 | "@nomiclabs/hardhat-etherscan": "^2.1.8", 42 | "@nomiclabs/hardhat-waffle": "^2.0.1", 43 | "@typechain/ethers-v5": "^4.0.0", 44 | "@types/chai": "^4.2.6", 45 | "@types/mocha": "^5.2.7", 46 | "chai": "^4.2.0", 47 | "decimal.js": "^10.2.1", 48 | "ethereum-waffle": "^3.0.2", 49 | "ethers": "^5.0.8", 50 | "hardhat": "^2.6.8", 51 | "hardhat-typechain": "^0.3.5", 52 | "is-svg": "^4.3.1", 53 | "mocha": "^6.2.2", 54 | "mocha-chai-jest-snapshot": "^1.1.0", 55 | "prettier": "^2.0.5", 56 | "prettier-plugin-solidity": "^1.0.0-beta.10", 57 | "solhint": "^3.2.1", 58 | "solhint-plugin-prettier": "^0.0.5", 59 | "ts-generator": "^0.1.1", 60 | "ts-node": "^8.5.4", 61 | "typechain": "^4.0.0", 62 | "typescript": "^3.7.3" 63 | }, 64 | "scripts": { 65 | "compile": "hardhat compile", 66 | "test": "hardhat test" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap Swap Router 2 | 3 | [![Tests](https://github.com/Uniswap/swap-router-contracts/workflows/Tests/badge.svg)](https://github.com/Uniswap/swap-router-contracts/actions?query=workflow%3ATests) 4 | [![Lint](https://github.com/Uniswap/swap-router-contracts/workflows/Lint/badge.svg)](https://github.com/Uniswap/swap-router-contracts/actions?query=workflow%3ALint) 5 | 6 | This repository contains smart contracts for swapping on the Uniswap V2 and V3 protocols. 7 | 8 | ## Bug bounty 9 | 10 | This repository is subject to the Uniswap V3 bug bounty program, 11 | per the terms defined [here](./bug-bounty.md). 12 | 13 | ## Local deployment 14 | 15 | In order to deploy this code to a local testnet, you should install the npm package 16 | `@uniswap/swap-router-contracts` 17 | and import bytecode imported from artifacts located at 18 | `@uniswap/swap-router-contracts/artifacts/contracts/*/*.json`. 19 | For example: 20 | 21 | ```typescript 22 | import { 23 | abi as SWAP_ROUTER_ABI, 24 | bytecode as SWAP_ROUTER_BYTECODE, 25 | } from '@uniswap/swap-router-contracts/artifacts/contracts/SwapRouter02.sol/SwapRouter02.json' 26 | 27 | // deploy the bytecode 28 | ``` 29 | 30 | This will ensure that you are testing against the same bytecode that is deployed to 31 | mainnet and public testnets, and all Uniswap code will correctly interoperate with 32 | your local deployment. 33 | 34 | ## Using solidity interfaces 35 | 36 | The swap router contract interfaces are available for import into solidity smart contracts 37 | via the npm artifact `@uniswap/swap-router-contracts`, e.g.: 38 | 39 | ```solidity 40 | import '@uniswap/swap-router-contracts/contracts/interfaces/ISwapRouter02.sol'; 41 | 42 | contract MyContract { 43 | ISwapRouter02 router; 44 | 45 | function doSomethingWithSwapRouter() { 46 | // router.exactInput(...); 47 | } 48 | } 49 | 50 | ``` 51 | 52 | ## Tests 53 | 54 | Some tests use Hardhat mainnet forking and therefore require an archive node. 55 | Either create a `.env` file in the workspace root containing: 56 | 57 | ``` 58 | ARCHIVE_RPC_URL='...' 59 | ``` 60 | 61 | Or set the variable when running tests: 62 | 63 | ``` 64 | export ARCHIVE_RPC_URL='...' && npm run test 65 | ``` 66 | -------------------------------------------------------------------------------- /contracts/test/TestUniswapV3Callee.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 5 | import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; 6 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 7 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 8 | 9 | contract TestUniswapV3Callee is IUniswapV3SwapCallback { 10 | using SafeCast for uint256; 11 | 12 | function swapExact0For1( 13 | address pool, 14 | uint256 amount0In, 15 | address recipient, 16 | uint160 sqrtPriceLimitX96 17 | ) external { 18 | IUniswapV3Pool(pool).swap(recipient, true, amount0In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); 19 | } 20 | 21 | function swap0ForExact1( 22 | address pool, 23 | uint256 amount1Out, 24 | address recipient, 25 | uint160 sqrtPriceLimitX96 26 | ) external { 27 | IUniswapV3Pool(pool).swap(recipient, true, -amount1Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); 28 | } 29 | 30 | function swapExact1For0( 31 | address pool, 32 | uint256 amount1In, 33 | address recipient, 34 | uint160 sqrtPriceLimitX96 35 | ) external { 36 | IUniswapV3Pool(pool).swap(recipient, false, amount1In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); 37 | } 38 | 39 | function swap1ForExact0( 40 | address pool, 41 | uint256 amount0Out, 42 | address recipient, 43 | uint160 sqrtPriceLimitX96 44 | ) external { 45 | IUniswapV3Pool(pool).swap(recipient, false, -amount0Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender)); 46 | } 47 | 48 | function uniswapV3SwapCallback( 49 | int256 amount0Delta, 50 | int256 amount1Delta, 51 | bytes calldata data 52 | ) external override { 53 | address sender = abi.decode(data, (address)); 54 | 55 | if (amount0Delta > 0) { 56 | IERC20(IUniswapV3Pool(msg.sender).token0()).transferFrom(sender, msg.sender, uint256(amount0Delta)); 57 | } else { 58 | assert(amount1Delta > 0); 59 | IERC20(IUniswapV3Pool(msg.sender).token1()).transferFrom(sender, msg.sender, uint256(amount1Delta)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/__snapshots__/QuoterV2.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 -> 1 1`] = `277146`; 4 | 5 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 0 tick, starting tick initialized 1`] = `123797`; 6 | 7 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 0 tick, starting tick not initialized 1`] = `100962`; 8 | 9 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 1 tick 1`] = `144724`; 10 | 11 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 2 tick 1`] = `182321`; 12 | 13 | exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 2 tick where after is initialized 1`] = `144762`; 14 | 15 | exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 0 tick, starting tick initialized 1`] = `97654`; 16 | 17 | exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 0 tick, starting tick not initialized 1`] = `93779`; 18 | 19 | exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 2 1`] = `175133`; 20 | 21 | exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 2 where tick after is initialized 1`] = `175141`; 22 | 23 | exports[`QuoterV2 quotes #quoteExactInput 2 -> 1 1`] = `97329`; 24 | 25 | exports[`QuoterV2 quotes #quoteExactInputSingle 0 -> 2 1`] = `182303`; 26 | 27 | exports[`QuoterV2 quotes #quoteExactInputSingle 2 -> 0 1`] = `175105`; 28 | 29 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 -> 1 1`] = `276746`; 30 | 31 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 0 tick starting tick initialized 1`] = `123352`; 32 | 33 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 0 tick starting tick not initialized 1`] = `100537`; 34 | 35 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 1 tick 1`] = `144010`; 36 | 37 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 2 tick 1`] = `181363`; 38 | 39 | exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 2 where tick after is initialized 1`] = `144048`; 40 | 41 | exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 1 tick 1`] = `137858`; 42 | 43 | exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 2 ticks 1`] = `175203`; 44 | 45 | exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 2 where tick after is initialized 1`] = `175197`; 46 | 47 | exports[`QuoterV2 quotes #quoteExactOutput 2 -> 1 1`] = `97870`; 48 | 49 | exports[`QuoterV2 quotes #quoteExactOutputSingle 0 -> 1 1`] = `105200`; 50 | 51 | exports[`QuoterV2 quotes #quoteExactOutputSingle 1 -> 0 1`] = `98511`; 52 | -------------------------------------------------------------------------------- /test/shared/externalFixtures.ts: -------------------------------------------------------------------------------- 1 | import { 2 | abi as FACTORY_ABI, 3 | bytecode as FACTORY_BYTECODE, 4 | } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json' 5 | import { abi as FACTORY_V2_ABI, bytecode as FACTORY_V2_BYTECODE } from '@uniswap/v2-core/build/UniswapV2Factory.json' 6 | import { Fixture } from 'ethereum-waffle' 7 | import { ethers, waffle } from 'hardhat' 8 | import { IWETH9, MockTimeSwapRouter02 } from '../../typechain' 9 | 10 | import WETH9 from '../contracts/WETH9.json' 11 | import { Contract } from '@ethersproject/contracts' 12 | import { constants } from 'ethers' 13 | 14 | import { 15 | abi as NFT_POSITION_MANAGER_ABI, 16 | bytecode as NFT_POSITION_MANAGER_BYTECODE, 17 | } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' 18 | 19 | const wethFixture: Fixture<{ weth9: IWETH9 }> = async ([wallet]) => { 20 | const weth9 = (await waffle.deployContract(wallet, { 21 | bytecode: WETH9.bytecode, 22 | abi: WETH9.abi, 23 | })) as IWETH9 24 | 25 | return { weth9 } 26 | } 27 | 28 | export const v2FactoryFixture: Fixture<{ factory: Contract }> = async ([wallet]) => { 29 | const factory = await waffle.deployContract( 30 | wallet, 31 | { 32 | bytecode: FACTORY_V2_BYTECODE, 33 | abi: FACTORY_V2_ABI, 34 | }, 35 | [constants.AddressZero] 36 | ) 37 | 38 | return { factory } 39 | } 40 | 41 | const v3CoreFactoryFixture: Fixture = async ([wallet]) => { 42 | return await waffle.deployContract(wallet, { 43 | bytecode: FACTORY_BYTECODE, 44 | abi: FACTORY_ABI, 45 | }) 46 | } 47 | 48 | export const v3RouterFixture: Fixture<{ 49 | weth9: IWETH9 50 | factoryV2: Contract 51 | factory: Contract 52 | nft: Contract 53 | router: MockTimeSwapRouter02 54 | }> = async ([wallet], provider) => { 55 | const { weth9 } = await wethFixture([wallet], provider) 56 | const { factory: factoryV2 } = await v2FactoryFixture([wallet], provider) 57 | const factory = await v3CoreFactoryFixture([wallet], provider) 58 | 59 | const nft = await waffle.deployContract( 60 | wallet, 61 | { 62 | bytecode: NFT_POSITION_MANAGER_BYTECODE, 63 | abi: NFT_POSITION_MANAGER_ABI, 64 | }, 65 | [factory.address, weth9.address, constants.AddressZero] 66 | ) 67 | 68 | const router = (await (await ethers.getContractFactory('MockTimeSwapRouter02')).deploy( 69 | factoryV2.address, 70 | factory.address, 71 | nft.address, 72 | weth9.address 73 | )) as MockTimeSwapRouter02 74 | 75 | return { weth9, factoryV2, factory, nft, router } 76 | } 77 | -------------------------------------------------------------------------------- /contracts/test/MockObservations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity =0.7.6; 3 | 4 | import '@uniswap/v3-core/contracts/libraries/Oracle.sol'; 5 | 6 | contract MockObservations { 7 | using Oracle for Oracle.Observation[65535]; 8 | 9 | // slot0 10 | int24 private slot0Tick; 11 | uint16 private slot0ObservationCardinality; 12 | uint16 private slot0ObservationIndex; 13 | 14 | // observations 15 | Oracle.Observation[65535] public observations; 16 | 17 | // block timestamps always monotonic increasing from 0, cumulative ticks are calculated automatically 18 | constructor( 19 | uint32[3] memory blockTimestamps, 20 | int24[3] memory ticks, 21 | bool mockLowObservationCardinality 22 | ) { 23 | require(blockTimestamps[0] == 0, '0'); 24 | require(blockTimestamps[1] > 0, '1'); 25 | require(blockTimestamps[2] > blockTimestamps[1], '2'); 26 | 27 | int56 tickCumulative = 0; 28 | for (uint256 i = 0; i < blockTimestamps.length; i++) { 29 | if (i != 0) { 30 | int24 tick = ticks[i - 1]; 31 | uint32 delta = blockTimestamps[i] - blockTimestamps[i - 1]; 32 | tickCumulative += int56(tick) * delta; 33 | } 34 | observations[i] = Oracle.Observation({ 35 | blockTimestamp: blockTimestamps[i], 36 | tickCumulative: tickCumulative, 37 | secondsPerLiquidityCumulativeX128: uint160(i), 38 | initialized: true 39 | }); 40 | } 41 | slot0Tick = ticks[2]; 42 | slot0ObservationCardinality = mockLowObservationCardinality ? 1 : 3; 43 | slot0ObservationIndex = 2; 44 | } 45 | 46 | function slot0() 47 | external 48 | view 49 | returns ( 50 | uint160, 51 | int24, 52 | uint16, 53 | uint16, 54 | uint16, 55 | uint8, 56 | bool 57 | ) 58 | { 59 | return (0, slot0Tick, slot0ObservationIndex, slot0ObservationCardinality, 0, 0, false); 60 | } 61 | 62 | function observe(uint32[] calldata secondsAgos) 63 | external 64 | view 65 | returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) 66 | { 67 | return 68 | observations.observe( 69 | observations[2].blockTimestamp, 70 | secondsAgos, 71 | slot0Tick, 72 | slot0ObservationIndex, 73 | 0, 74 | slot0ObservationCardinality 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/interfaces/IApproveAndCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | interface IApproveAndCall { 6 | enum ApprovalType {NOT_REQUIRED, MAX, MAX_MINUS_ONE, ZERO_THEN_MAX, ZERO_THEN_MAX_MINUS_ONE} 7 | 8 | /// @dev Lens to be called off-chain to determine which (if any) of the relevant approval functions should be called 9 | /// @param token The token to approve 10 | /// @param amount The amount to approve 11 | /// @return The required approval type 12 | function getApprovalType(address token, uint256 amount) external returns (ApprovalType); 13 | 14 | /// @notice Approves a token for the maximum possible amount 15 | /// @param token The token to approve 16 | function approveMax(address token) external payable; 17 | 18 | /// @notice Approves a token for the maximum possible amount minus one 19 | /// @param token The token to approve 20 | function approveMaxMinusOne(address token) external payable; 21 | 22 | /// @notice Approves a token for zero, then the maximum possible amount 23 | /// @param token The token to approve 24 | function approveZeroThenMax(address token) external payable; 25 | 26 | /// @notice Approves a token for zero, then the maximum possible amount minus one 27 | /// @param token The token to approve 28 | function approveZeroThenMaxMinusOne(address token) external payable; 29 | 30 | /// @notice Calls the position manager with arbitrary calldata 31 | /// @param data Calldata to pass along to the position manager 32 | /// @return result The result from the call 33 | function callPositionManager(bytes memory data) external payable returns (bytes memory result); 34 | 35 | struct MintParams { 36 | address token0; 37 | address token1; 38 | uint24 fee; 39 | int24 tickLower; 40 | int24 tickUpper; 41 | uint256 amount0Min; 42 | uint256 amount1Min; 43 | address recipient; 44 | } 45 | 46 | /// @notice Calls the position manager's mint function 47 | /// @param params Calldata to pass along to the position manager 48 | /// @return result The result from the call 49 | function mint(MintParams calldata params) external payable returns (bytes memory result); 50 | 51 | struct IncreaseLiquidityParams { 52 | address token0; 53 | address token1; 54 | uint256 tokenId; 55 | uint256 amount0Min; 56 | uint256 amount1Min; 57 | } 58 | 59 | /// @notice Calls the position manager's increaseLiquidity function 60 | /// @param params Calldata to pass along to the position manager 61 | /// @return result The result from the call 62 | function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (bytes memory result); 63 | } 64 | -------------------------------------------------------------------------------- /contracts/interfaces/ITokenValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @notice Validates tokens by flash borrowing from the token/ pool on V2. 6 | /// @notice Returns 7 | /// Status.FOT if we detected a fee is taken on transfer. 8 | /// Status.STF if transfer failed for the token. 9 | /// Status.UNKN if we did not detect any issues with the token. 10 | /// @notice A return value of Status.UNKN does not mean the token is definitely not a fee on transfer token 11 | /// or definitely has no problems with its transfer. It just means we cant say for sure that it has any 12 | /// issues. 13 | /// @dev We can not guarantee the result of this lens is correct for a few reasons: 14 | /// @dev 1/ Some tokens take fees or allow transfers under specific conditions, for example some have an allowlist 15 | /// @dev of addresses that do/dont require fees. Therefore the result is not guaranteed to be correct 16 | /// @dev in all circumstances. 17 | /// @dev 2/ It is possible that the token does not have any pools on V2 therefore we are not able to perform 18 | /// @dev a flashloan to test the token. 19 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 20 | /// to compute the result. 21 | interface ITokenValidator { 22 | // Status.FOT: detected a fee is taken on transfer. 23 | // Status.STF: transfer failed for the token. 24 | // Status.UNKN: no issues found with the token. 25 | enum Status {UNKN, FOT, STF} 26 | 27 | /// @notice Validates a token by detecting if its transferable or takes a fee on transfer 28 | /// @param token The address of the token to check for fee on transfer 29 | /// @param baseTokens The addresses of the tokens to try pairing with 30 | /// token when looking for a pool to flash loan from. 31 | /// @param amountToBorrow The amount to try flash borrowing from the pools 32 | /// @return The status of the token 33 | function validate( 34 | address token, 35 | address[] calldata baseTokens, 36 | uint256 amountToBorrow 37 | ) external returns (Status); 38 | 39 | /// @notice Validates each token by detecting if its transferable or takes a fee on transfer 40 | /// @param tokens The addresses of the tokens to check for fee on transfer 41 | /// @param baseTokens The addresses of the tokens to try pairing with 42 | /// token when looking for a pool to flash loan from. 43 | /// @param amountToBorrow The amount to try flash borrowing from the pools 44 | /// @return The status of the token 45 | function batchValidate( 46 | address[] calldata tokens, 47 | address[] calldata baseTokens, 48 | uint256 amountToBorrow 49 | ) external returns (Status[] memory); 50 | } 51 | -------------------------------------------------------------------------------- /contracts/interfaces/IQuoter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title Quoter Interface 6 | /// @notice Supports quoting the calculated amounts from exact input or exact output swaps 7 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 8 | /// to compute the result. They are also not gas efficient and should not be called on-chain. 9 | interface IQuoter { 10 | /// @notice Returns the amount out received for a given exact input swap without executing the swap 11 | /// @param path The path of the swap, i.e. each token pair and the pool fee 12 | /// @param amountIn The amount of the first token to swap 13 | /// @return amountOut The amount of the last token that would be received 14 | function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut); 15 | 16 | /// @notice Returns the amount out received for a given exact input but for a swap of a single pool 17 | /// @param tokenIn The token being swapped in 18 | /// @param tokenOut The token being swapped out 19 | /// @param fee The fee of the token pool to consider for the pair 20 | /// @param amountIn The desired input amount 21 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 22 | /// @return amountOut The amount of `tokenOut` that would be received 23 | function quoteExactInputSingle( 24 | address tokenIn, 25 | address tokenOut, 26 | uint24 fee, 27 | uint256 amountIn, 28 | uint160 sqrtPriceLimitX96 29 | ) external returns (uint256 amountOut); 30 | 31 | /// @notice Returns the amount in required for a given exact output swap without executing the swap 32 | /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order 33 | /// @param amountOut The amount of the last token to receive 34 | /// @return amountIn The amount of first token required to be paid 35 | function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn); 36 | 37 | /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool 38 | /// @param tokenIn The token being swapped in 39 | /// @param tokenOut The token being swapped out 40 | /// @param fee The fee of the token pool to consider for the pair 41 | /// @param amountOut The desired output amount 42 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 43 | /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` 44 | function quoteExactOutputSingle( 45 | address tokenIn, 46 | address tokenOut, 47 | uint24 fee, 48 | uint256 amountOut, 49 | uint160 sqrtPriceLimitX96 50 | ) external returns (uint256 amountIn); 51 | } 52 | -------------------------------------------------------------------------------- /contracts/interfaces/IV3SwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 6 | 7 | /// @title Router token swapping functionality 8 | /// @notice Functions for swapping tokens via Uniswap V3 9 | interface IV3SwapRouter is IUniswapV3SwapCallback { 10 | struct ExactInputSingleParams { 11 | address tokenIn; 12 | address tokenOut; 13 | uint24 fee; 14 | address recipient; 15 | uint256 amountIn; 16 | uint256 amountOutMinimum; 17 | uint160 sqrtPriceLimitX96; 18 | } 19 | 20 | /// @notice Swaps `amountIn` of one token for as much as possible of another token 21 | /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, 22 | /// and swap the entire amount, enabling contracts to send tokens before calling this function. 23 | /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata 24 | /// @return amountOut The amount of the received token 25 | function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); 26 | 27 | struct ExactInputParams { 28 | bytes path; 29 | address recipient; 30 | uint256 amountIn; 31 | uint256 amountOutMinimum; 32 | } 33 | 34 | /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path 35 | /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance, 36 | /// and swap the entire amount, enabling contracts to send tokens before calling this function. 37 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata 38 | /// @return amountOut The amount of the received token 39 | function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); 40 | 41 | struct ExactOutputSingleParams { 42 | address tokenIn; 43 | address tokenOut; 44 | uint24 fee; 45 | address recipient; 46 | uint256 amountOut; 47 | uint256 amountInMaximum; 48 | uint160 sqrtPriceLimitX96; 49 | } 50 | 51 | /// @notice Swaps as little as possible of one token for `amountOut` of another token 52 | /// that may remain in the router after the swap. 53 | /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata 54 | /// @return amountIn The amount of the input token 55 | function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); 56 | 57 | struct ExactOutputParams { 58 | bytes path; 59 | address recipient; 60 | uint256 amountOut; 61 | uint256 amountInMaximum; 62 | } 63 | 64 | /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) 65 | /// that may remain in the router after the swap. 66 | /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata 67 | /// @return amountIn The amount of the input token 68 | function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); 69 | } 70 | -------------------------------------------------------------------------------- /contracts/libraries/UniswapV2Library.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 5 | import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol'; 6 | 7 | library UniswapV2Library { 8 | using LowGasSafeMath for uint256; 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); 13 | (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 14 | require(token0 != address(0)); 15 | } 16 | 17 | // calculates the CREATE2 address for a pair without making any external calls 18 | function pairFor( 19 | address factory, 20 | address tokenA, 21 | address tokenB 22 | ) internal pure returns (address pair) { 23 | (address token0, address token1) = sortTokens(tokenA, tokenB); 24 | pair = address( 25 | uint256( 26 | keccak256( 27 | abi.encodePacked( 28 | hex'ff', 29 | factory, 30 | keccak256(abi.encodePacked(token0, token1)), 31 | hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash 32 | ) 33 | ) 34 | ) 35 | ); 36 | } 37 | 38 | // fetches and sorts the reserves for a pair 39 | function getReserves( 40 | address factory, 41 | address tokenA, 42 | address tokenB 43 | ) internal view returns (uint256 reserveA, uint256 reserveB) { 44 | (address token0, ) = sortTokens(tokenA, tokenB); 45 | (uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); 46 | (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 47 | } 48 | 49 | // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset 50 | function getAmountOut( 51 | uint256 amountIn, 52 | uint256 reserveIn, 53 | uint256 reserveOut 54 | ) internal pure returns (uint256 amountOut) { 55 | require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT'); 56 | require(reserveIn > 0 && reserveOut > 0); 57 | uint256 amountInWithFee = amountIn.mul(997); 58 | uint256 numerator = amountInWithFee.mul(reserveOut); 59 | uint256 denominator = reserveIn.mul(1000).add(amountInWithFee); 60 | amountOut = numerator / denominator; 61 | } 62 | 63 | // given an output amount of an asset and pair reserves, returns a required input amount of the other asset 64 | function getAmountIn( 65 | uint256 amountOut, 66 | uint256 reserveIn, 67 | uint256 reserveOut 68 | ) internal pure returns (uint256 amountIn) { 69 | require(amountOut > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); 70 | require(reserveIn > 0 && reserveOut > 0); 71 | uint256 numerator = reserveIn.mul(amountOut).mul(1000); 72 | uint256 denominator = reserveOut.sub(amountOut).mul(997); 73 | amountIn = (numerator / denominator).add(1); 74 | } 75 | 76 | // performs chained getAmountIn calculations on any number of pairs 77 | function getAmountsIn( 78 | address factory, 79 | uint256 amountOut, 80 | address[] memory path 81 | ) internal view returns (uint256[] memory amounts) { 82 | require(path.length >= 2); 83 | amounts = new uint256[](path.length); 84 | amounts[amounts.length - 1] = amountOut; 85 | for (uint256 i = path.length - 1; i > 0; i--) { 86 | (uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]); 87 | amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/interfaces/IMixedRouteQuoterV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title MixedRouteQuoterV1 Interface 6 | /// @notice Supports quoting the calculated amounts for exact input swaps. Is specialized for routes containing a mix of V2 and V3 liquidity. 7 | /// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. 8 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 9 | /// to compute the result. They are also not gas efficient and should not be called on-chain. 10 | interface IMixedRouteQuoterV1 { 11 | /// @notice Returns the amount out received for a given exact input swap without executing the swap 12 | /// @param path The path of the swap, i.e. each token pair and the pool fee 13 | /// @param amountIn The amount of the first token to swap 14 | /// @return amountOut The amount of the last token that would be received 15 | /// @return v3SqrtPriceX96AfterList List of the sqrt price after the swap for each v3 pool in the path, 0 for v2 pools 16 | /// @return v3InitializedTicksCrossedList List of the initialized ticks that the swap crossed for each v3 pool in the path, 0 for v2 pools 17 | /// @return v3SwapGasEstimate The estimate of the gas that the v3 swaps in the path consume 18 | function quoteExactInput(bytes memory path, uint256 amountIn) 19 | external 20 | returns ( 21 | uint256 amountOut, 22 | uint160[] memory v3SqrtPriceX96AfterList, 23 | uint32[] memory v3InitializedTicksCrossedList, 24 | uint256 v3SwapGasEstimate 25 | ); 26 | 27 | struct QuoteExactInputSingleV3Params { 28 | address tokenIn; 29 | address tokenOut; 30 | uint256 amountIn; 31 | uint24 fee; 32 | uint160 sqrtPriceLimitX96; 33 | } 34 | 35 | struct QuoteExactInputSingleV2Params { 36 | address tokenIn; 37 | address tokenOut; 38 | uint256 amountIn; 39 | } 40 | 41 | /// @notice Returns the amount out received for a given exact input but for a swap of a single pool 42 | /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` 43 | /// tokenIn The token being swapped in 44 | /// tokenOut The token being swapped out 45 | /// fee The fee of the token pool to consider for the pair 46 | /// amountIn The desired input amount 47 | /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 48 | /// @return amountOut The amount of `tokenOut` that would be received 49 | /// @return sqrtPriceX96After The sqrt price of the pool after the swap 50 | /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed 51 | /// @return gasEstimate The estimate of the gas that the swap consumes 52 | function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params) 53 | external 54 | returns ( 55 | uint256 amountOut, 56 | uint160 sqrtPriceX96After, 57 | uint32 initializedTicksCrossed, 58 | uint256 gasEstimate 59 | ); 60 | 61 | /// @notice Returns the amount out received for a given exact input but for a swap of a single V2 pool 62 | /// @param params The params for the quote, encoded as `QuoteExactInputSingleV2Params` 63 | /// tokenIn The token being swapped in 64 | /// tokenOut The token being swapped out 65 | /// amountIn The desired input amount 66 | /// @return amountOut The amount of `tokenOut` that would be received 67 | function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params) external returns (uint256 amountOut); 68 | 69 | /// @dev ExactOutput swaps are not supported by this new Quoter which is specialized for supporting routes 70 | /// crossing both V2 liquidity pairs and V3 pools. 71 | /// @deprecated quoteExactOutputSingle and exactOutput. Use QuoterV2 instead. 72 | } 73 | -------------------------------------------------------------------------------- /contracts/V2SwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol'; 6 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 7 | 8 | import './interfaces/IV2SwapRouter.sol'; 9 | import './base/ImmutableState.sol'; 10 | import './base/PeripheryPaymentsWithFeeExtended.sol'; 11 | import './libraries/Constants.sol'; 12 | import './libraries/UniswapV2Library.sol'; 13 | 14 | /// @title Uniswap V2 Swap Router 15 | /// @notice Router for stateless execution of swaps against Uniswap V2 16 | abstract contract V2SwapRouter is IV2SwapRouter, ImmutableState, PeripheryPaymentsWithFeeExtended { 17 | using LowGasSafeMath for uint256; 18 | 19 | // supports fee-on-transfer tokens 20 | // requires the initial amount to have already been sent to the first pair 21 | function _swap(address[] memory path, address _to) private { 22 | for (uint256 i; i < path.length - 1; i++) { 23 | (address input, address output) = (path[i], path[i + 1]); 24 | (address token0, ) = UniswapV2Library.sortTokens(input, output); 25 | IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factoryV2, input, output)); 26 | uint256 amountInput; 27 | uint256 amountOutput; 28 | // scope to avoid stack too deep errors 29 | { 30 | (uint256 reserve0, uint256 reserve1, ) = pair.getReserves(); 31 | (uint256 reserveInput, uint256 reserveOutput) = 32 | input == token0 ? (reserve0, reserve1) : (reserve1, reserve0); 33 | amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput); 34 | amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput); 35 | } 36 | (uint256 amount0Out, uint256 amount1Out) = 37 | input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0)); 38 | address to = i < path.length - 2 ? UniswapV2Library.pairFor(factoryV2, output, path[i + 2]) : _to; 39 | pair.swap(amount0Out, amount1Out, to, new bytes(0)); 40 | } 41 | } 42 | 43 | /// @inheritdoc IV2SwapRouter 44 | function swapExactTokensForTokens( 45 | uint256 amountIn, 46 | uint256 amountOutMin, 47 | address[] calldata path, 48 | address to 49 | ) external payable override returns (uint256 amountOut) { 50 | // use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract 51 | bool hasAlreadyPaid; 52 | if (amountIn == Constants.CONTRACT_BALANCE) { 53 | hasAlreadyPaid = true; 54 | amountIn = IERC20(path[0]).balanceOf(address(this)); 55 | } 56 | 57 | pay( 58 | path[0], 59 | hasAlreadyPaid ? address(this) : msg.sender, 60 | UniswapV2Library.pairFor(factoryV2, path[0], path[1]), 61 | amountIn 62 | ); 63 | 64 | // find and replace to addresses 65 | if (to == Constants.MSG_SENDER) to = msg.sender; 66 | else if (to == Constants.ADDRESS_THIS) to = address(this); 67 | 68 | uint256 balanceBefore = IERC20(path[path.length - 1]).balanceOf(to); 69 | 70 | _swap(path, to); 71 | 72 | amountOut = IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore); 73 | require(amountOut >= amountOutMin, 'Too little received'); 74 | } 75 | 76 | /// @inheritdoc IV2SwapRouter 77 | function swapTokensForExactTokens( 78 | uint256 amountOut, 79 | uint256 amountInMax, 80 | address[] calldata path, 81 | address to 82 | ) external payable override returns (uint256 amountIn) { 83 | amountIn = UniswapV2Library.getAmountsIn(factoryV2, amountOut, path)[0]; 84 | require(amountIn <= amountInMax, 'Too much requested'); 85 | 86 | pay(path[0], msg.sender, UniswapV2Library.pairFor(factoryV2, path[0], path[1]), amountIn); 87 | 88 | // find and replace to addresses 89 | if (to == Constants.MSG_SENDER) to = msg.sender; 90 | else if (to == Constants.ADDRESS_THIS) to = address(this); 91 | 92 | _swap(path, to); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/libraries/PoolTicksCounter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.6.0; 3 | 4 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 5 | 6 | library PoolTicksCounter { 7 | /// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter. 8 | /// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the 9 | /// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do 10 | /// want to count tickAfter. The opposite is true if we are swapping downwards. 11 | function countInitializedTicksCrossed( 12 | IUniswapV3Pool self, 13 | int24 tickBefore, 14 | int24 tickAfter 15 | ) internal view returns (uint32 initializedTicksCrossed) { 16 | int16 wordPosLower; 17 | int16 wordPosHigher; 18 | uint8 bitPosLower; 19 | uint8 bitPosHigher; 20 | bool tickBeforeInitialized; 21 | bool tickAfterInitialized; 22 | 23 | { 24 | // Get the key and offset in the tick bitmap of the active tick before and after the swap. 25 | int16 wordPos = int16((tickBefore / self.tickSpacing()) >> 8); 26 | uint8 bitPos = uint8((tickBefore / self.tickSpacing()) % 256); 27 | 28 | int16 wordPosAfter = int16((tickAfter / self.tickSpacing()) >> 8); 29 | uint8 bitPosAfter = uint8((tickAfter / self.tickSpacing()) % 256); 30 | 31 | // In the case where tickAfter is initialized, we only want to count it if we are swapping downwards. 32 | // If the initializable tick after the swap is initialized, our original tickAfter is a 33 | // multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized 34 | // and we shouldn't count it. 35 | tickAfterInitialized = 36 | ((self.tickBitmap(wordPosAfter) & (1 << bitPosAfter)) > 0) && 37 | ((tickAfter % self.tickSpacing()) == 0) && 38 | (tickBefore > tickAfter); 39 | 40 | // In the case where tickBefore is initialized, we only want to count it if we are swapping upwards. 41 | // Use the same logic as above to decide whether we should count tickBefore or not. 42 | tickBeforeInitialized = 43 | ((self.tickBitmap(wordPos) & (1 << bitPos)) > 0) && 44 | ((tickBefore % self.tickSpacing()) == 0) && 45 | (tickBefore < tickAfter); 46 | 47 | if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) { 48 | wordPosLower = wordPos; 49 | bitPosLower = bitPos; 50 | wordPosHigher = wordPosAfter; 51 | bitPosHigher = bitPosAfter; 52 | } else { 53 | wordPosLower = wordPosAfter; 54 | bitPosLower = bitPosAfter; 55 | wordPosHigher = wordPos; 56 | bitPosHigher = bitPos; 57 | } 58 | } 59 | 60 | // Count the number of initialized ticks crossed by iterating through the tick bitmap. 61 | // Our first mask should include the lower tick and everything to its left. 62 | uint256 mask = type(uint256).max << bitPosLower; 63 | while (wordPosLower <= wordPosHigher) { 64 | // If we're on the final tick bitmap page, ensure we only count up to our 65 | // ending tick. 66 | if (wordPosLower == wordPosHigher) { 67 | mask = mask & (type(uint256).max >> (255 - bitPosHigher)); 68 | } 69 | 70 | uint256 masked = self.tickBitmap(wordPosLower) & mask; 71 | initializedTicksCrossed += countOneBits(masked); 72 | wordPosLower++; 73 | // Reset our mask so we consider all bits on the next iteration. 74 | mask = type(uint256).max; 75 | } 76 | 77 | if (tickAfterInitialized) { 78 | initializedTicksCrossed -= 1; 79 | } 80 | 81 | if (tickBeforeInitialized) { 82 | initializedTicksCrossed -= 1; 83 | } 84 | 85 | return initializedTicksCrossed; 86 | } 87 | 88 | function countOneBits(uint256 x) private pure returns (uint16) { 89 | uint16 bits = 0; 90 | while (x != 0) { 91 | bits++; 92 | x &= (x - 1); 93 | } 94 | return bits; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /contracts/interfaces/IQuoterV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title QuoterV2 Interface 6 | /// @notice Supports quoting the calculated amounts from exact input or exact output swaps. 7 | /// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. 8 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 9 | /// to compute the result. They are also not gas efficient and should not be called on-chain. 10 | interface IQuoterV2 { 11 | /// @notice Returns the amount out received for a given exact input swap without executing the swap 12 | /// @param path The path of the swap, i.e. each token pair and the pool fee 13 | /// @param amountIn The amount of the first token to swap 14 | /// @return amountOut The amount of the last token that would be received 15 | /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path 16 | /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path 17 | /// @return gasEstimate The estimate of the gas that the swap consumes 18 | function quoteExactInput(bytes memory path, uint256 amountIn) 19 | external 20 | returns ( 21 | uint256 amountOut, 22 | uint160[] memory sqrtPriceX96AfterList, 23 | uint32[] memory initializedTicksCrossedList, 24 | uint256 gasEstimate 25 | ); 26 | 27 | struct QuoteExactInputSingleParams { 28 | address tokenIn; 29 | address tokenOut; 30 | uint256 amountIn; 31 | uint24 fee; 32 | uint160 sqrtPriceLimitX96; 33 | } 34 | 35 | /// @notice Returns the amount out received for a given exact input but for a swap of a single pool 36 | /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` 37 | /// tokenIn The token being swapped in 38 | /// tokenOut The token being swapped out 39 | /// fee The fee of the token pool to consider for the pair 40 | /// amountIn The desired input amount 41 | /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 42 | /// @return amountOut The amount of `tokenOut` that would be received 43 | /// @return sqrtPriceX96After The sqrt price of the pool after the swap 44 | /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed 45 | /// @return gasEstimate The estimate of the gas that the swap consumes 46 | function quoteExactInputSingle(QuoteExactInputSingleParams memory params) 47 | external 48 | returns ( 49 | uint256 amountOut, 50 | uint160 sqrtPriceX96After, 51 | uint32 initializedTicksCrossed, 52 | uint256 gasEstimate 53 | ); 54 | 55 | /// @notice Returns the amount in required for a given exact output swap without executing the swap 56 | /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order 57 | /// @param amountOut The amount of the last token to receive 58 | /// @return amountIn The amount of first token required to be paid 59 | /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path 60 | /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path 61 | /// @return gasEstimate The estimate of the gas that the swap consumes 62 | function quoteExactOutput(bytes memory path, uint256 amountOut) 63 | external 64 | returns ( 65 | uint256 amountIn, 66 | uint160[] memory sqrtPriceX96AfterList, 67 | uint32[] memory initializedTicksCrossedList, 68 | uint256 gasEstimate 69 | ); 70 | 71 | struct QuoteExactOutputSingleParams { 72 | address tokenIn; 73 | address tokenOut; 74 | uint256 amount; 75 | uint24 fee; 76 | uint160 sqrtPriceLimitX96; 77 | } 78 | 79 | /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool 80 | /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` 81 | /// tokenIn The token being swapped in 82 | /// tokenOut The token being swapped out 83 | /// fee The fee of the token pool to consider for the pair 84 | /// amountOut The desired output amount 85 | /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 86 | /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` 87 | /// @return sqrtPriceX96After The sqrt price of the pool after the swap 88 | /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed 89 | /// @return gasEstimate The estimate of the gas that the swap consumes 90 | function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) 91 | external 92 | returns ( 93 | uint256 amountIn, 94 | uint160 sqrtPriceX96After, 95 | uint32 initializedTicksCrossed, 96 | uint256 gasEstimate 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /test/shared/quoter.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, Contract } from 'ethers' 2 | import { FeeAmount, TICK_SPACINGS } from './constants' 3 | import { encodePriceSqrt } from './encodePriceSqrt' 4 | import { getMaxTick, getMinTick } from './ticks' 5 | 6 | export async function createPool(nft: Contract, wallet: Wallet, tokenAddressA: string, tokenAddressB: string) { 7 | if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase()) 8 | [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA] 9 | 10 | await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1)) 11 | 12 | const liquidityParams = { 13 | token0: tokenAddressA, 14 | token1: tokenAddressB, 15 | fee: FeeAmount.MEDIUM, 16 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 17 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 18 | recipient: wallet.address, 19 | amount0Desired: 1000000, 20 | amount1Desired: 1000000, 21 | amount0Min: 0, 22 | amount1Min: 0, 23 | deadline: 2 ** 32, 24 | } 25 | 26 | return nft.mint(liquidityParams) 27 | } 28 | 29 | export async function createPoolWithMultiplePositions( 30 | nft: Contract, 31 | wallet: Wallet, 32 | tokenAddressA: string, 33 | tokenAddressB: string 34 | ) { 35 | if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase()) 36 | [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA] 37 | 38 | await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1)) 39 | 40 | const liquidityParams = { 41 | token0: tokenAddressA, 42 | token1: tokenAddressB, 43 | fee: FeeAmount.MEDIUM, 44 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 45 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 46 | recipient: wallet.address, 47 | amount0Desired: 1000000, 48 | amount1Desired: 1000000, 49 | amount0Min: 0, 50 | amount1Min: 0, 51 | deadline: 2 ** 32, 52 | } 53 | 54 | await nft.mint(liquidityParams) 55 | 56 | const liquidityParams2 = { 57 | token0: tokenAddressA, 58 | token1: tokenAddressB, 59 | fee: FeeAmount.MEDIUM, 60 | tickLower: -60, 61 | tickUpper: 60, 62 | recipient: wallet.address, 63 | amount0Desired: 100, 64 | amount1Desired: 100, 65 | amount0Min: 0, 66 | amount1Min: 0, 67 | deadline: 2 ** 32, 68 | } 69 | 70 | await nft.mint(liquidityParams2) 71 | 72 | const liquidityParams3 = { 73 | token0: tokenAddressA, 74 | token1: tokenAddressB, 75 | fee: FeeAmount.MEDIUM, 76 | tickLower: -120, 77 | tickUpper: 120, 78 | recipient: wallet.address, 79 | amount0Desired: 100, 80 | amount1Desired: 100, 81 | amount0Min: 0, 82 | amount1Min: 0, 83 | deadline: 2 ** 32, 84 | } 85 | 86 | return nft.mint(liquidityParams3) 87 | } 88 | 89 | export async function createPoolWithZeroTickInitialized( 90 | nft: Contract, 91 | wallet: Wallet, 92 | tokenAddressA: string, 93 | tokenAddressB: string 94 | ) { 95 | if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase()) 96 | [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA] 97 | 98 | await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1)) 99 | 100 | const liquidityParams = { 101 | token0: tokenAddressA, 102 | token1: tokenAddressB, 103 | fee: FeeAmount.MEDIUM, 104 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 105 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 106 | recipient: wallet.address, 107 | amount0Desired: 1000000, 108 | amount1Desired: 1000000, 109 | amount0Min: 0, 110 | amount1Min: 0, 111 | deadline: 2 ** 32, 112 | } 113 | 114 | await nft.mint(liquidityParams) 115 | 116 | const liquidityParams2 = { 117 | token0: tokenAddressA, 118 | token1: tokenAddressB, 119 | fee: FeeAmount.MEDIUM, 120 | tickLower: 0, 121 | tickUpper: 60, 122 | recipient: wallet.address, 123 | amount0Desired: 100, 124 | amount1Desired: 100, 125 | amount0Min: 0, 126 | amount1Min: 0, 127 | deadline: 2 ** 32, 128 | } 129 | 130 | await nft.mint(liquidityParams2) 131 | 132 | const liquidityParams3 = { 133 | token0: tokenAddressA, 134 | token1: tokenAddressB, 135 | fee: FeeAmount.MEDIUM, 136 | tickLower: -120, 137 | tickUpper: 0, 138 | recipient: wallet.address, 139 | amount0Desired: 100, 140 | amount1Desired: 100, 141 | amount0Min: 0, 142 | amount1Min: 0, 143 | deadline: 2 ** 32, 144 | } 145 | 146 | return nft.mint(liquidityParams3) 147 | } 148 | 149 | /** 150 | * Create V2 pairs for testing with IL routes 151 | */ 152 | export async function createPair(v2Factory: Contract, tokenAddressA: string, tokenAddressB: string): Promise { 153 | // .createPair() sorts the tokens already 154 | const receipt = await (await v2Factory.createPair(tokenAddressA, tokenAddressB)).wait() 155 | // we can extract the pair address from the emitted event 156 | // always the 3rd element: emit PairCreated(token0, token1, pair, allPairs.length); 157 | const pairAddress = receipt.events[0].args[2] 158 | if (!pairAddress) throw new Error('pairAddress not found in txn receipt') 159 | return pairAddress 160 | } 161 | -------------------------------------------------------------------------------- /contracts/base/ApproveAndCall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 6 | import '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol'; 7 | 8 | import '../interfaces/IApproveAndCall.sol'; 9 | import './ImmutableState.sol'; 10 | 11 | /// @title Approve and Call 12 | /// @notice Allows callers to approve the Uniswap V3 position manager from this contract, 13 | /// for any token, and then make calls into the position manager 14 | abstract contract ApproveAndCall is IApproveAndCall, ImmutableState { 15 | function tryApprove(address token, uint256 amount) private returns (bool) { 16 | (bool success, bytes memory data) = 17 | token.call(abi.encodeWithSelector(IERC20.approve.selector, positionManager, amount)); 18 | return success && (data.length == 0 || abi.decode(data, (bool))); 19 | } 20 | 21 | /// @inheritdoc IApproveAndCall 22 | function getApprovalType(address token, uint256 amount) external override returns (ApprovalType) { 23 | // check existing approval 24 | if (IERC20(token).allowance(address(this), positionManager) >= amount) return ApprovalType.NOT_REQUIRED; 25 | 26 | // try type(uint256).max / type(uint256).max - 1 27 | if (tryApprove(token, type(uint256).max)) return ApprovalType.MAX; 28 | if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.MAX_MINUS_ONE; 29 | 30 | // set approval to 0 (must succeed) 31 | require(tryApprove(token, 0)); 32 | 33 | // try type(uint256).max / type(uint256).max - 1 34 | if (tryApprove(token, type(uint256).max)) return ApprovalType.ZERO_THEN_MAX; 35 | if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.ZERO_THEN_MAX_MINUS_ONE; 36 | 37 | revert(); 38 | } 39 | 40 | /// @inheritdoc IApproveAndCall 41 | function approveMax(address token) external payable override { 42 | require(tryApprove(token, type(uint256).max)); 43 | } 44 | 45 | /// @inheritdoc IApproveAndCall 46 | function approveMaxMinusOne(address token) external payable override { 47 | require(tryApprove(token, type(uint256).max - 1)); 48 | } 49 | 50 | /// @inheritdoc IApproveAndCall 51 | function approveZeroThenMax(address token) external payable override { 52 | require(tryApprove(token, 0)); 53 | require(tryApprove(token, type(uint256).max)); 54 | } 55 | 56 | /// @inheritdoc IApproveAndCall 57 | function approveZeroThenMaxMinusOne(address token) external payable override { 58 | require(tryApprove(token, 0)); 59 | require(tryApprove(token, type(uint256).max - 1)); 60 | } 61 | 62 | /// @inheritdoc IApproveAndCall 63 | function callPositionManager(bytes memory data) public payable override returns (bytes memory result) { 64 | bool success; 65 | (success, result) = positionManager.call(data); 66 | 67 | if (!success) { 68 | // Next 5 lines from https://ethereum.stackexchange.com/a/83577 69 | if (result.length < 68) revert(); 70 | assembly { 71 | result := add(result, 0x04) 72 | } 73 | revert(abi.decode(result, (string))); 74 | } 75 | } 76 | 77 | function balanceOf(address token) private view returns (uint256) { 78 | return IERC20(token).balanceOf(address(this)); 79 | } 80 | 81 | /// @inheritdoc IApproveAndCall 82 | function mint(MintParams calldata params) external payable override returns (bytes memory result) { 83 | return 84 | callPositionManager( 85 | abi.encodeWithSelector( 86 | INonfungiblePositionManager.mint.selector, 87 | INonfungiblePositionManager.MintParams({ 88 | token0: params.token0, 89 | token1: params.token1, 90 | fee: params.fee, 91 | tickLower: params.tickLower, 92 | tickUpper: params.tickUpper, 93 | amount0Desired: balanceOf(params.token0), 94 | amount1Desired: balanceOf(params.token1), 95 | amount0Min: params.amount0Min, 96 | amount1Min: params.amount1Min, 97 | recipient: params.recipient, 98 | deadline: type(uint256).max // deadline should be checked via multicall 99 | }) 100 | ) 101 | ); 102 | } 103 | 104 | /// @inheritdoc IApproveAndCall 105 | function increaseLiquidity(IncreaseLiquidityParams calldata params) 106 | external 107 | payable 108 | override 109 | returns (bytes memory result) 110 | { 111 | return 112 | callPositionManager( 113 | abi.encodeWithSelector( 114 | INonfungiblePositionManager.increaseLiquidity.selector, 115 | INonfungiblePositionManager.IncreaseLiquidityParams({ 116 | tokenId: params.tokenId, 117 | amount0Desired: balanceOf(params.token0), 118 | amount1Desired: balanceOf(params.token1), 119 | amount0Min: params.amount0Min, 120 | amount1Min: params.amount1Min, 121 | deadline: type(uint256).max // deadline should be checked via multicall 122 | }) 123 | ) 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /bug-bounty.md: -------------------------------------------------------------------------------- 1 | # Uniswap V3 Bug Bounty 2 | 3 | ## Overview 4 | 5 | Starting on September 16th, 2021, the [swap-router-contracts](https://github.com/Uniswap/swap-router-contracts) repository is 6 | subject to the Uniswap V3 Bug Bounty (the “Program”) to incentivize responsible bug disclosure. 7 | 8 | We are limiting the scope of the Program to critical and high severity bugs, and are offering a reward of up to $500,000. Happy hunting! 9 | 10 | ## Scope 11 | 12 | The scope of the Program is limited to bugs that result in the loss of user funds. 13 | 14 | The following are not within the scope of the Program: 15 | 16 | - Any contract located under [contracts/test](./contracts/test) or [contracts/lens](./contracts/lens). 17 | - Bugs in any third party contract or platform. 18 | - Vulnerabilities already reported and/or discovered in contracts built by third parties. 19 | - Any already-reported bugs. 20 | 21 | Vulnerabilities contingent upon the occurrence of any of the following also are outside the scope of this Program: 22 | 23 | - Frontend bugs 24 | - DDOS attacks 25 | - Spamming 26 | - Phishing 27 | - Automated tools (Github Actions, AWS, etc.) 28 | - Compromise or misuse of third party systems or services 29 | 30 | ## Assumptions 31 | 32 | Uniswap V3 was developed with the following assumptions, and thus any bug must also adhere to the following assumptions 33 | to be eligible for the bug bounty: 34 | 35 | - The total supply of any token does not exceed 2128 - 1, i.e. `type(uint128).max`. 36 | - The `transfer` and `transferFrom` methods of any token strictly decrease the balance of the token sender by the transfer amount and increases the balance of token recipient by the transfer amount, i.e. fee on transfer tokens are excluded. 37 | - The token balance of an address can only change due to a call to `transfer` by the sender or `transferFrom` by an approved address, i.e. rebase tokens and interest bearing tokens are excluded. 38 | 39 | ## Rewards 40 | 41 | Rewards will be allocated based on the severity of the bug disclosed and will be evaluated and rewarded at the discretion of the Uniswap Labs team. 42 | For critical bugs that lead to loss of user funds (more than 1% or user specified slippage tolerance), 43 | rewards of up to $500,000 will be granted. Lower severity bugs will be rewarded at the discretion of the team. 44 | In addition, all vulnerabilities disclosed prior to the mainnet launch date will be subject to receive higher rewards. 45 | 46 | ## Disclosure 47 | 48 | Any vulnerability or bug discovered must be reported only to the following email: [security@uniswap.org](mailto:security@uniswap.org). 49 | 50 | The vulnerability must not be disclosed publicly or to any other person, entity or email address before Uniswap Labs has been notified, has fixed the issue, and has granted permission for public disclosure. In addition, disclosure must be made within 24 hours following discovery of the vulnerability. 51 | 52 | A detailed report of a vulnerability increases the likelihood of a reward and may increase the reward amount. Please provide as much information about the vulnerability as possible, including: 53 | 54 | - The conditions on which reproducing the bug is contingent. 55 | - The steps needed to reproduce the bug or, preferably, a proof of concept. 56 | - The potential implications of the vulnerability being abused. 57 | 58 | Anyone who reports a unique, previously-unreported vulnerability that results in a change to the code or a configuration change and who keeps such vulnerability confidential until it has been resolved by our engineers will be recognized publicly for their contribution if they so choose. 59 | 60 | ## Eligibility 61 | 62 | To be eligible for a reward under this Program, you must: 63 | 64 | - Discover a previously unreported, non-public vulnerability that would result in a loss of and/or lock on any ERC-20 token on Uniswap V2 or V3 (but not on any third party platform) and that is within the scope of this Program. Vulnerabilities must be distinct from the issues covered in the Trail of Bits or ABDK audits. 65 | - Be the first to disclose the unique vulnerability to [security@uniswap.org](mailto:security@uniswap.org), in compliance with the disclosure requirements above. If similar vulnerabilities are reported within the same 24 hour period, rewards will be split at the discretion of Uniswap Labs. 66 | - Provide sufficient information to enable our engineers to reproduce and fix the vulnerability. 67 | - Not engage in any unlawful conduct when disclosing the bug, including through threats, demands, or any other coercive tactics. 68 | - Not exploit the vulnerability in any way, including through making it public or by obtaining a profit (other than a reward under this Program). 69 | - Make a good faith effort to avoid privacy violations, destruction of data, interruption or degradation of Uniswap V2 or V3. 70 | - Submit only one vulnerability per submission, unless you need to chain vulnerabilities to provide impact regarding any of the vulnerabilities. 71 | - Not submit a vulnerability caused by an underlying issue that is the same as an issue on which a reward has been paid under this Program. 72 | - Not be one of our current or former employees, vendors, or contractors or an employee of any of those vendors or contractors. 73 | - Not be subject to US sanctions or reside in a US-embargoed country. 74 | - Be at least 18 years of age or, if younger, submit your vulnerability with the consent of your parent or guardian. 75 | 76 | ## Other Terms 77 | 78 | By submitting your report, you grant Uniswap Labs any and all rights, including intellectual property rights, needed to validate, mitigate, and disclose the vulnerability. All reward decisions, including eligibility for and amounts of the rewards and the manner in which such rewards will be paid, are made at our sole discretion. 79 | 80 | The terms and conditions of this Program may be altered at any time. 81 | -------------------------------------------------------------------------------- /test/TokenValidator.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { constants } from 'ethers' 3 | import hre, { ethers } from 'hardhat' 4 | import { TokenValidator, TestERC20, IUniswapV2Pair__factory } from '../typechain' 5 | 6 | describe('TokenValidator', function () { 7 | let tokenValidator: TokenValidator 8 | let testToken: TestERC20 9 | 10 | this.timeout(100000) 11 | 12 | enum Status { 13 | UNKN = 0, 14 | FOT = 1, 15 | STF = 2, 16 | } 17 | 18 | // WETH9 and USDC 19 | const BASE_TOKENS = ['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'] 20 | // Arbitrary amount to flash loan. 21 | const AMOUNT_TO_BORROW = 1000 22 | 23 | const FOT_TOKENS = [ 24 | '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF 25 | '0x8B3192f5eEBD8579568A2Ed41E6FEB402f93f73F', // SAITAMA 26 | '0xA2b4C0Af19cC16a6CfAcCe81F192B024d625817D', // KISHU 27 | ] 28 | 29 | const BROKEN_TOKENS = [ 30 | '0xd233d1f6fd11640081abb8db125f722b5dc729dc', // USD 31 | ] 32 | 33 | const NON_FOT_TOKENS = [ 34 | '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC 35 | '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', // UNI 36 | '0xc00e94Cb662C3520282E6f5717214004A7f26888', // COMP 37 | '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH9 38 | ] 39 | 40 | before(async function () { 41 | // Easiest to test FOT using real world data, so these tests require a hardhat fork. 42 | if (!process.env.ARCHIVE_RPC_URL) { 43 | this.skip() 44 | } 45 | 46 | await hre.network.provider.request({ 47 | method: 'hardhat_reset', 48 | params: [ 49 | { 50 | forking: { 51 | jsonRpcUrl: process.env.ARCHIVE_RPC_URL, 52 | blockNumber: 14024832, 53 | }, 54 | }, 55 | ], 56 | }) 57 | 58 | const factory = await ethers.getContractFactory('TokenValidator') 59 | tokenValidator = (await factory.deploy( 60 | '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', // V2 Factory 61 | '0xC36442b4a4522E871399CD717aBDD847Ab11FE88' // V3 NFT position manager 62 | )) as TokenValidator 63 | 64 | // Deploy a new token for testing. 65 | const tokenFactory = await ethers.getContractFactory('TestERC20') 66 | testToken = (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20 67 | }) 68 | 69 | after(async () => { 70 | // Disable mainnet forking to avoid effecting other tests. 71 | await hre.network.provider.request({ 72 | method: 'hardhat_reset', 73 | params: [], 74 | }) 75 | }) 76 | 77 | it('succeeds for tokens that cant be transferred', async () => { 78 | for (const token of BROKEN_TOKENS) { 79 | const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW) 80 | expect(isFot).to.equal(Status.STF) 81 | } 82 | }) 83 | 84 | it('succeeds to detect fot tokens', async () => { 85 | for (const token of FOT_TOKENS) { 86 | const isFot = await tokenValidator.callStatic.validate(token, [BASE_TOKENS[0]!], AMOUNT_TO_BORROW) 87 | expect(isFot).to.equal(Status.FOT) 88 | } 89 | }) 90 | 91 | it('succeeds to detect fot token when token doesnt have pair with first base token', async () => { 92 | const isFot = await tokenValidator.callStatic.validate( 93 | FOT_TOKENS[0], 94 | [testToken.address, ...BASE_TOKENS], 95 | AMOUNT_TO_BORROW 96 | ) 97 | expect(isFot).to.equal(Status.FOT) 98 | }) 99 | 100 | it('succeeds to return unknown when flash loaning full reserves', async () => { 101 | const pairAddress = '0xab293dce330b92aa52bc2a7cd3816edaa75f890b' // WTF/ETH pair 102 | const pair = IUniswapV2Pair__factory.connect(pairAddress, ethers.provider) 103 | const { reserve0: wtfReserve } = await pair.callStatic.getReserves() 104 | 105 | const isFot1 = await tokenValidator.callStatic.validate( 106 | '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF 107 | ['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // WETH 108 | wtfReserve.sub(1).toString() 109 | ) 110 | expect(isFot1).to.equal(Status.FOT) 111 | 112 | const isFot2 = await tokenValidator.callStatic.validate( 113 | '0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF 114 | ['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // WETH 115 | wtfReserve.toString() 116 | ) 117 | expect(isFot2).to.equal(Status.UNKN) 118 | }) 119 | 120 | it('succeeds to batch detect fot tokens', async () => { 121 | const isFots = await tokenValidator.callStatic.batchValidate(FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW) 122 | expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true 123 | }) 124 | 125 | it('succeeds to batch detect fot tokens when dont have pair with first base token', async () => { 126 | const isFots = await tokenValidator.callStatic.batchValidate( 127 | FOT_TOKENS, 128 | [testToken.address, ...BASE_TOKENS], 129 | AMOUNT_TO_BORROW 130 | ) 131 | expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true 132 | }) 133 | 134 | it('succeeds to detect non fot tokens', async () => { 135 | for (const token of NON_FOT_TOKENS) { 136 | const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW) 137 | expect(isFot).to.equal(Status.UNKN) 138 | } 139 | }) 140 | 141 | it('succeeds to batch detect non fot tokens', async () => { 142 | const isFots = await tokenValidator.callStatic.batchValidate(NON_FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW) 143 | expect(isFots.every((isFot: Status) => isFot == Status.UNKN)).to.be.true 144 | }) 145 | 146 | it('succeeds to batch detect mix of fot tokens and non fot tokens', async () => { 147 | const isFots = await tokenValidator.callStatic.batchValidate( 148 | [NON_FOT_TOKENS[0], FOT_TOKENS[0], BROKEN_TOKENS[0]], 149 | BASE_TOKENS, 150 | 1000 151 | ) 152 | expect(isFots).to.deep.equal([Status.UNKN, Status.FOT, Status.STF]) 153 | }) 154 | 155 | it('succeeds to return false if token doesnt have a pool with any of the base tokens', async () => { 156 | await tokenValidator.callStatic.validate(testToken.address, BASE_TOKENS, AMOUNT_TO_BORROW) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /test/Quoter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Fixture } from 'ethereum-waffle' 2 | import { constants, Wallet, Contract } from 'ethers' 3 | import { ethers, waffle } from 'hardhat' 4 | import { Quoter, TestERC20 } from '../typechain' 5 | import completeFixture from './shared/completeFixture' 6 | import { FeeAmount, MaxUint128, TICK_SPACINGS } from './shared/constants' 7 | import { encodePriceSqrt } from './shared/encodePriceSqrt' 8 | import { expandTo18Decimals } from './shared/expandTo18Decimals' 9 | import { expect } from './shared/expect' 10 | import { encodePath } from './shared/path' 11 | import { createPool } from './shared/quoter' 12 | 13 | describe('Quoter', () => { 14 | let wallet: Wallet 15 | let trader: Wallet 16 | 17 | const swapRouterFixture: Fixture<{ 18 | nft: Contract 19 | tokens: [TestERC20, TestERC20, TestERC20] 20 | quoter: Quoter 21 | }> = async (wallets, provider) => { 22 | const { weth9, factory, router, tokens, nft } = await completeFixture(wallets, provider) 23 | 24 | // approve & fund wallets 25 | for (const token of tokens) { 26 | await token.approve(router.address, constants.MaxUint256) 27 | await token.approve(nft.address, constants.MaxUint256) 28 | await token.connect(trader).approve(router.address, constants.MaxUint256) 29 | await token.transfer(trader.address, expandTo18Decimals(1_000_000)) 30 | } 31 | 32 | const quoterFactory = await ethers.getContractFactory('Quoter') 33 | quoter = (await quoterFactory.deploy(factory.address, weth9.address)) as Quoter 34 | 35 | return { 36 | tokens, 37 | nft, 38 | quoter, 39 | } 40 | } 41 | 42 | let nft: Contract 43 | let tokens: [TestERC20, TestERC20, TestERC20] 44 | let quoter: Quoter 45 | 46 | let loadFixture: ReturnType 47 | 48 | before('create fixture loader', async () => { 49 | const wallets = await (ethers as any).getSigners() 50 | ;[wallet, trader] = wallets 51 | loadFixture = waffle.createFixtureLoader(wallets) 52 | }) 53 | 54 | // helper for getting weth and token balances 55 | beforeEach('load fixture', async () => { 56 | ;({ tokens, nft, quoter } = await loadFixture(swapRouterFixture)) 57 | }) 58 | 59 | describe('quotes', () => { 60 | beforeEach(async () => { 61 | await createPool(nft, wallet, tokens[0].address, tokens[1].address) 62 | await createPool(nft, wallet, tokens[1].address, tokens[2].address) 63 | }) 64 | 65 | describe('#quoteExactInput', () => { 66 | it('0 -> 1', async () => { 67 | const quote = await quoter.callStatic.quoteExactInput( 68 | encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]), 69 | 3 70 | ) 71 | 72 | expect(quote).to.eq(1) 73 | }) 74 | 75 | it('1 -> 0', async () => { 76 | const quote = await quoter.callStatic.quoteExactInput( 77 | encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]), 78 | 3 79 | ) 80 | 81 | expect(quote).to.eq(1) 82 | }) 83 | 84 | it('0 -> 1 -> 2', async () => { 85 | const quote = await quoter.callStatic.quoteExactInput( 86 | encodePath( 87 | tokens.map((token) => token.address), 88 | [FeeAmount.MEDIUM, FeeAmount.MEDIUM] 89 | ), 90 | 5 91 | ) 92 | 93 | expect(quote).to.eq(1) 94 | }) 95 | 96 | it('2 -> 1 -> 0', async () => { 97 | const quote = await quoter.callStatic.quoteExactInput( 98 | encodePath(tokens.map((token) => token.address).reverse(), [FeeAmount.MEDIUM, FeeAmount.MEDIUM]), 99 | 5 100 | ) 101 | 102 | expect(quote).to.eq(1) 103 | }) 104 | }) 105 | 106 | describe('#quoteExactInputSingle', () => { 107 | it('0 -> 1', async () => { 108 | const quote = await quoter.callStatic.quoteExactInputSingle( 109 | tokens[0].address, 110 | tokens[1].address, 111 | FeeAmount.MEDIUM, 112 | MaxUint128, 113 | // -2% 114 | encodePriceSqrt(100, 102) 115 | ) 116 | 117 | expect(quote).to.eq(9852) 118 | }) 119 | 120 | it('1 -> 0', async () => { 121 | const quote = await quoter.callStatic.quoteExactInputSingle( 122 | tokens[1].address, 123 | tokens[0].address, 124 | FeeAmount.MEDIUM, 125 | MaxUint128, 126 | // +2% 127 | encodePriceSqrt(102, 100) 128 | ) 129 | 130 | expect(quote).to.eq(9852) 131 | }) 132 | }) 133 | 134 | describe('#quoteExactOutput', () => { 135 | it('0 -> 1', async () => { 136 | const quote = await quoter.callStatic.quoteExactOutput( 137 | encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]), 138 | 1 139 | ) 140 | 141 | expect(quote).to.eq(3) 142 | }) 143 | 144 | it('1 -> 0', async () => { 145 | const quote = await quoter.callStatic.quoteExactOutput( 146 | encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]), 147 | 1 148 | ) 149 | 150 | expect(quote).to.eq(3) 151 | }) 152 | 153 | it('0 -> 1 -> 2', async () => { 154 | const quote = await quoter.callStatic.quoteExactOutput( 155 | encodePath(tokens.map((token) => token.address).reverse(), [FeeAmount.MEDIUM, FeeAmount.MEDIUM]), 156 | 1 157 | ) 158 | 159 | expect(quote).to.eq(5) 160 | }) 161 | 162 | it('2 -> 1 -> 0', async () => { 163 | const quote = await quoter.callStatic.quoteExactOutput( 164 | encodePath( 165 | tokens.map((token) => token.address), 166 | [FeeAmount.MEDIUM, FeeAmount.MEDIUM] 167 | ), 168 | 1 169 | ) 170 | 171 | expect(quote).to.eq(5) 172 | }) 173 | }) 174 | 175 | describe('#quoteExactOutputSingle', () => { 176 | it('0 -> 1', async () => { 177 | const quote = await quoter.callStatic.quoteExactOutputSingle( 178 | tokens[0].address, 179 | tokens[1].address, 180 | FeeAmount.MEDIUM, 181 | MaxUint128, 182 | encodePriceSqrt(100, 102) 183 | ) 184 | 185 | expect(quote).to.eq(9981) 186 | }) 187 | 188 | it('1 -> 0', async () => { 189 | const quote = await quoter.callStatic.quoteExactOutputSingle( 190 | tokens[1].address, 191 | tokens[0].address, 192 | FeeAmount.MEDIUM, 193 | MaxUint128, 194 | encodePriceSqrt(102, 100) 195 | ) 196 | 197 | expect(quote).to.eq(9981) 198 | }) 199 | }) 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /contracts/lens/TokenValidator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 6 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 7 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol'; 8 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 9 | import '../libraries/UniswapV2Library.sol'; 10 | import '../interfaces/ISwapRouter02.sol'; 11 | import '../interfaces/ITokenValidator.sol'; 12 | import '../base/ImmutableState.sol'; 13 | 14 | /// @notice Validates tokens by flash borrowing from the token/ pool on V2. 15 | /// @notice Returns 16 | /// Status.FOT if we detected a fee is taken on transfer. 17 | /// Status.STF if transfer failed for the token. 18 | /// Status.UNKN if we did not detect any issues with the token. 19 | /// @notice A return value of Status.UNKN does not mean the token is definitely not a fee on transfer token 20 | /// or definitely has no problems with its transfer. It just means we cant say for sure that it has any 21 | /// issues. 22 | /// @dev We can not guarantee the result of this lens is correct for a few reasons: 23 | /// @dev 1/ Some tokens take fees or allow transfers under specific conditions, for example some have an allowlist 24 | /// @dev of addresses that do/dont require fees. Therefore the result is not guaranteed to be correct 25 | /// @dev in all circumstances. 26 | /// @dev 2/ It is possible that the token does not have any pools on V2 therefore we are not able to perform 27 | /// @dev a flashloan to test the token. 28 | contract TokenValidator is ITokenValidator, IUniswapV2Callee, ImmutableState { 29 | string internal constant FOT_REVERT_STRING = 'FOT'; 30 | // https://github.com/Uniswap/v2-core/blob/1136544ac842ff48ae0b1b939701436598d74075/contracts/UniswapV2Pair.sol#L46 31 | string internal constant STF_REVERT_STRING_SUFFIX = 'TRANSFER_FAILED'; 32 | 33 | constructor(address _factoryV2, address _positionManager) ImmutableState(_factoryV2, _positionManager) {} 34 | 35 | function batchValidate( 36 | address[] calldata tokens, 37 | address[] calldata baseTokens, 38 | uint256 amountToBorrow 39 | ) public override returns (Status[] memory isFotResults) { 40 | isFotResults = new Status[](tokens.length); 41 | for (uint256 i = 0; i < tokens.length; i++) { 42 | isFotResults[i] = validate(tokens[i], baseTokens, amountToBorrow); 43 | } 44 | } 45 | 46 | function validate( 47 | address token, 48 | address[] calldata baseTokens, 49 | uint256 amountToBorrow 50 | ) public override returns (Status) { 51 | for (uint256 i = 0; i < baseTokens.length; i++) { 52 | Status result = _validate(token, baseTokens[i], amountToBorrow); 53 | if (result == Status.FOT || result == Status.STF) { 54 | return result; 55 | } 56 | } 57 | return Status.UNKN; 58 | } 59 | 60 | function _validate( 61 | address token, 62 | address baseToken, 63 | uint256 amountToBorrow 64 | ) internal returns (Status) { 65 | if (token == baseToken) { 66 | return Status.UNKN; 67 | } 68 | 69 | address pairAddress = UniswapV2Library.pairFor(this.factoryV2(), token, baseToken); 70 | 71 | // If the token/baseToken pair exists, get token0. 72 | // Must do low level call as try/catch does not support case where contract does not exist. 73 | (, bytes memory returnData) = address(pairAddress).call(abi.encodeWithSelector(IUniswapV2Pair.token0.selector)); 74 | 75 | if (returnData.length == 0) { 76 | return Status.UNKN; 77 | } 78 | 79 | address token0Address = abi.decode(returnData, (address)); 80 | 81 | // Flash loan {amountToBorrow} 82 | (uint256 amount0Out, uint256 amount1Out) = 83 | token == token0Address ? (amountToBorrow, uint256(0)) : (uint256(0), amountToBorrow); 84 | 85 | uint256 balanceBeforeLoan = IERC20(token).balanceOf(address(this)); 86 | 87 | IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); 88 | 89 | try 90 | pair.swap(amount0Out, amount1Out, address(this), abi.encode(balanceBeforeLoan, amountToBorrow)) 91 | {} catch Error(string memory reason) { 92 | if (isFotFailed(reason)) { 93 | return Status.FOT; 94 | } 95 | 96 | if (isTransferFailed(reason)) { 97 | return Status.STF; 98 | } 99 | 100 | return Status.UNKN; 101 | } 102 | 103 | // Swap always reverts so should never reach. 104 | revert('Unexpected error'); 105 | } 106 | 107 | function isFotFailed(string memory reason) internal pure returns (bool) { 108 | return keccak256(bytes(reason)) == keccak256(bytes(FOT_REVERT_STRING)); 109 | } 110 | 111 | function isTransferFailed(string memory reason) internal pure returns (bool) { 112 | // We check the suffix of the revert string so we can support forks that 113 | // may have modified the prefix. 114 | string memory stf = STF_REVERT_STRING_SUFFIX; 115 | 116 | uint256 reasonLength = bytes(reason).length; 117 | uint256 suffixLength = bytes(stf).length; 118 | if (reasonLength < suffixLength) { 119 | return false; 120 | } 121 | 122 | uint256 ptr; 123 | uint256 offset = 32 + reasonLength - suffixLength; 124 | bool transferFailed; 125 | assembly { 126 | ptr := add(reason, offset) 127 | let suffixPtr := add(stf, 32) 128 | transferFailed := eq(keccak256(ptr, suffixLength), keccak256(suffixPtr, suffixLength)) 129 | } 130 | 131 | return transferFailed; 132 | } 133 | 134 | function uniswapV2Call( 135 | address, 136 | uint256 amount0, 137 | uint256, 138 | bytes calldata data 139 | ) external view override { 140 | IUniswapV2Pair pair = IUniswapV2Pair(msg.sender); 141 | (address token0, address token1) = (pair.token0(), pair.token1()); 142 | 143 | IERC20 tokenBorrowed = IERC20(amount0 > 0 ? token0 : token1); 144 | 145 | (uint256 balanceBeforeLoan, uint256 amountRequestedToBorrow) = abi.decode(data, (uint256, uint256)); 146 | uint256 amountBorrowed = tokenBorrowed.balanceOf(address(this)) - balanceBeforeLoan; 147 | 148 | // If we received less token than we requested when we called swap, then a fee must have been taken 149 | // by the token during transfer. 150 | if (amountBorrowed != amountRequestedToBorrow) { 151 | revert(FOT_REVERT_STRING); 152 | } 153 | 154 | // Note: If we do not revert here, we would end up reverting in the pair's swap method anyway 155 | // since for a flash borrow we need to transfer back the amount we borrowed + 0.3% fee, and we don't 156 | // have funds to cover the fee. Revert early here to save gas/time. 157 | revert('Unknown'); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /contracts/lens/Quoter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 6 | import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; 7 | import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; 8 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 9 | import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 10 | import '@uniswap/v3-periphery/contracts/libraries/Path.sol'; 11 | import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; 12 | import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol'; 13 | 14 | import '../interfaces/IQuoter.sol'; 15 | 16 | /// @title Provides quotes for swaps 17 | /// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap 18 | /// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute 19 | /// the swap and check the amounts in the callback. 20 | contract Quoter is IQuoter, IUniswapV3SwapCallback, PeripheryImmutableState { 21 | using Path for bytes; 22 | using SafeCast for uint256; 23 | 24 | /// @dev Transient storage variable used to check a safety condition in exact output swaps. 25 | uint256 private amountOutCached; 26 | 27 | constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {} 28 | 29 | function getPool( 30 | address tokenA, 31 | address tokenB, 32 | uint24 fee 33 | ) private view returns (IUniswapV3Pool) { 34 | return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); 35 | } 36 | 37 | /// @inheritdoc IUniswapV3SwapCallback 38 | function uniswapV3SwapCallback( 39 | int256 amount0Delta, 40 | int256 amount1Delta, 41 | bytes memory path 42 | ) external view override { 43 | require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported 44 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 45 | CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); 46 | 47 | (bool isExactInput, uint256 amountToPay, uint256 amountReceived) = 48 | amount0Delta > 0 49 | ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta)) 50 | : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta)); 51 | if (isExactInput) { 52 | assembly { 53 | let ptr := mload(0x40) 54 | mstore(ptr, amountReceived) 55 | revert(ptr, 32) 56 | } 57 | } else { 58 | // if the cache has been populated, ensure that the full output amount has been received 59 | if (amountOutCached != 0) require(amountReceived == amountOutCached); 60 | assembly { 61 | let ptr := mload(0x40) 62 | mstore(ptr, amountToPay) 63 | revert(ptr, 32) 64 | } 65 | } 66 | } 67 | 68 | /// @dev Parses a revert reason that should contain the numeric quote 69 | function parseRevertReason(bytes memory reason) private pure returns (uint256) { 70 | if (reason.length != 32) { 71 | if (reason.length < 68) revert('Unexpected error'); 72 | assembly { 73 | reason := add(reason, 0x04) 74 | } 75 | revert(abi.decode(reason, (string))); 76 | } 77 | return abi.decode(reason, (uint256)); 78 | } 79 | 80 | /// @inheritdoc IQuoter 81 | function quoteExactInputSingle( 82 | address tokenIn, 83 | address tokenOut, 84 | uint24 fee, 85 | uint256 amountIn, 86 | uint160 sqrtPriceLimitX96 87 | ) public override returns (uint256 amountOut) { 88 | bool zeroForOne = tokenIn < tokenOut; 89 | 90 | try 91 | getPool(tokenIn, tokenOut, fee).swap( 92 | address(this), // address(0) might cause issues with some tokens 93 | zeroForOne, 94 | amountIn.toInt256(), 95 | sqrtPriceLimitX96 == 0 96 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 97 | : sqrtPriceLimitX96, 98 | abi.encodePacked(tokenIn, fee, tokenOut) 99 | ) 100 | {} catch (bytes memory reason) { 101 | return parseRevertReason(reason); 102 | } 103 | } 104 | 105 | /// @inheritdoc IQuoter 106 | function quoteExactInput(bytes memory path, uint256 amountIn) external override returns (uint256 amountOut) { 107 | while (true) { 108 | bool hasMultiplePools = path.hasMultiplePools(); 109 | 110 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 111 | 112 | // the outputs of prior swaps become the inputs to subsequent ones 113 | amountIn = quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0); 114 | 115 | // decide whether to continue or terminate 116 | if (hasMultiplePools) { 117 | path = path.skipToken(); 118 | } else { 119 | return amountIn; 120 | } 121 | } 122 | } 123 | 124 | /// @inheritdoc IQuoter 125 | function quoteExactOutputSingle( 126 | address tokenIn, 127 | address tokenOut, 128 | uint24 fee, 129 | uint256 amountOut, 130 | uint160 sqrtPriceLimitX96 131 | ) public override returns (uint256 amountIn) { 132 | bool zeroForOne = tokenIn < tokenOut; 133 | 134 | // if no price limit has been specified, cache the output amount for comparison in the swap callback 135 | if (sqrtPriceLimitX96 == 0) amountOutCached = amountOut; 136 | try 137 | getPool(tokenIn, tokenOut, fee).swap( 138 | address(this), // address(0) might cause issues with some tokens 139 | zeroForOne, 140 | -amountOut.toInt256(), 141 | sqrtPriceLimitX96 == 0 142 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 143 | : sqrtPriceLimitX96, 144 | abi.encodePacked(tokenOut, fee, tokenIn) 145 | ) 146 | {} catch (bytes memory reason) { 147 | if (sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache 148 | return parseRevertReason(reason); 149 | } 150 | } 151 | 152 | /// @inheritdoc IQuoter 153 | function quoteExactOutput(bytes memory path, uint256 amountOut) external override returns (uint256 amountIn) { 154 | while (true) { 155 | bool hasMultiplePools = path.hasMultiplePools(); 156 | 157 | (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool(); 158 | 159 | // the inputs of prior swaps become the outputs of subsequent ones 160 | amountOut = quoteExactOutputSingle(tokenIn, tokenOut, fee, amountOut, 0); 161 | 162 | // decide whether to continue or terminate 163 | if (hasMultiplePools) { 164 | path = path.skipToken(); 165 | } else { 166 | return amountOut; 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /test/MixedRouteQuoterV1.integ.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { BigNumber } from 'ethers' 3 | import { MixedRouteQuoterV1 } from '../typechain' 4 | 5 | import hre, { ethers } from 'hardhat' 6 | import { encodePath } from './shared/path' 7 | import { expandTo18Decimals, expandToNDecimals } from './shared/expandTo18Decimals' 8 | import { FeeAmount, V2_FEE_PLACEHOLDER } from './shared/constants' 9 | 10 | const V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984' 11 | const V2_FACTORY = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' 12 | 13 | const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' 14 | const USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7' 15 | const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' 16 | const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' 17 | const DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' 18 | 19 | /// @dev basic V2 routes 20 | const DAI_V2_UNI_V2_WETH = encodePath([DAI, UNI, WETH], [V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER]) 21 | const USDC_V2_UNI_V2_WETH = encodePath([USDC, UNI, WETH], [V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER]) 22 | 23 | /// @dev basic V3 routes 24 | const USDC_V3_USDT = encodePath([USDC, USDT], [FeeAmount.LOW]) 25 | const UNI_V3_WETH = encodePath([UNI, WETH], [FeeAmount.MEDIUM]) 26 | 27 | /// @dev stablecoin IL routes 28 | const USDT_V3_DAI_V2_USDC = encodePath([USDT, DAI, USDC], [FeeAmount.LOW, V2_FEE_PLACEHOLDER]) 29 | const DAI_V3_USDC_V2_USDT = encodePath([DAI, USDC, USDT], [100, V2_FEE_PLACEHOLDER]) 30 | 31 | /// @dev erc20 IL routes 32 | // V3 - V2 33 | const UNI_V3_WETH_V2_DAI = encodePath([UNI, WETH, DAI], [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER]) 34 | const USDC_V3_UNI_V2_WETH = encodePath([USDC, UNI, WETH], [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER]) 35 | // V2 - V3 36 | const UNI_V2_WETH_V3_DAI = encodePath([UNI, WETH, DAI], [V2_FEE_PLACEHOLDER, FeeAmount.MEDIUM]) 37 | 38 | /// @dev complex IL routes 39 | // (use two V3 pools) 40 | const DAI_V3_3000_UNI_V2_USDT_V3_3000_WETH = encodePath( 41 | [DAI, UNI, USDT, WETH], 42 | [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER, FeeAmount.MEDIUM] 43 | ) 44 | // (use two V2 pools) 45 | const DAI_V3_3000_UNI_V2_USDT_V2_WETH = encodePath( 46 | [DAI, UNI, USDT, WETH], 47 | [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER] 48 | ) 49 | 50 | describe('MixedRouteQuoterV1 integration tests', function () { 51 | let mixedRouteQuoter: MixedRouteQuoterV1 52 | 53 | this.timeout(100000) 54 | 55 | before(async function () { 56 | if (!process.env.ARCHIVE_RPC_URL) { 57 | this.skip() 58 | } 59 | 60 | await hre.network.provider.request({ 61 | method: 'hardhat_reset', 62 | params: [ 63 | { 64 | forking: { 65 | jsonRpcUrl: process.env.ARCHIVE_RPC_URL, 66 | blockNumber: 14390000, 67 | }, 68 | }, 69 | ], 70 | }) 71 | 72 | const MixedRouteQuoterV1Factory = await ethers.getContractFactory('MixedRouteQuoterV1') 73 | mixedRouteQuoter = (await MixedRouteQuoterV1Factory.deploy(V3_FACTORY, V2_FACTORY, WETH)) as MixedRouteQuoterV1 74 | }) 75 | 76 | after(async () => { 77 | // Disable mainnet forking to avoid effecting other tests. 78 | await hre.network.provider.request({ 79 | method: 'hardhat_reset', 80 | params: [], 81 | }) 82 | }) 83 | 84 | /** 85 | * Test values only valid starting at block 14390000 86 | */ 87 | it('sets block number correctly', async () => { 88 | const blockNumber = BigNumber.from( 89 | await hre.network.provider.request({ 90 | method: 'eth_blockNumber', 91 | params: [], 92 | }) 93 | ) 94 | /// @dev +1 so 14390001 since we just requested 95 | expect(blockNumber.eq(14390001)).to.be.true 96 | }) 97 | 98 | describe('quotes stablecoin only paths correctly', () => { 99 | /// @dev the amount must be expanded to the decimals of the first token in the path 100 | it('V3-V2 stablecoin path with 6 decimal in start of path', async () => { 101 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 102 | 'quoteExactInput(bytes,uint256)' 103 | ](USDT_V3_DAI_V2_USDC, expandToNDecimals(10000, 6)) 104 | 105 | expect(amountOut).eq(BigNumber.from('9966336832')) 106 | expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x10c6727487c45717095f'))).to.be.true 107 | }) 108 | 109 | it('V3-V2 stablecoin path with 6 decimal in middle of path', async () => { 110 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 111 | 'quoteExactInput(bytes,uint256)' 112 | ](DAI_V3_USDC_V2_USDT, expandTo18Decimals(10000)) 113 | 114 | expect(amountOut).eq(BigNumber.from('9959354898')) 115 | expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x10c715093f77e3073634'))).to.be.true 116 | }) 117 | }) 118 | 119 | describe('V2-V2 quotes', () => { 120 | it('quotes V2-V2 correctly', async () => { 121 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 122 | 'quoteExactInput(bytes,uint256)' 123 | ](DAI_V2_UNI_V2_WETH, expandTo18Decimals(10000)) 124 | 125 | expect(amountOut).eq(BigNumber.from('2035189623576328665')) 126 | expect(v3SqrtPriceX96AfterList.every((el) => el.eq(0))).to.be.true 127 | expect(v3InitializedTicksCrossedList.every((el) => el == 0)).to.be.true 128 | }) 129 | 130 | it('quotes V2 (6 decimal stablecoin) -V2 correctly', async () => { 131 | const { amountOut } = await mixedRouteQuoter.callStatic['quoteExactInput(bytes,uint256)']( 132 | USDC_V2_UNI_V2_WETH, 133 | expandToNDecimals(10000, 6) 134 | ) 135 | 136 | expect(amountOut).eq(BigNumber.from('1989381322826753150')) 137 | }) 138 | }) 139 | 140 | it('quotes V3-V2 erc20s with mixed decimal scales correctly', async () => { 141 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 142 | 'quoteExactInput(bytes,uint256)' 143 | ](USDC_V3_UNI_V2_WETH, expandToNDecimals(10000, 6)) 144 | 145 | expect(amountOut).eq(BigNumber.from('3801923847986895918')) // 3.801923847986895918 146 | expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x3110863ba621ac3915fd'))).to.be.true 147 | }) 148 | 149 | it('quotes V3-V2 correctly', async () => { 150 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 151 | 'quoteExactInput(bytes,uint256)' 152 | ](UNI_V3_WETH_V2_DAI, expandTo18Decimals(10000)) 153 | 154 | expect(amountOut).eq(BigNumber.from('80675538331724434694636')) 155 | expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x0e83f285cb58c4cca14fb78b'))).to.be.true 156 | }) 157 | 158 | it('quotes V3-V2-V3 correctly', async () => { 159 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 160 | 'quoteExactInput(bytes,uint256)' 161 | ](DAI_V3_3000_UNI_V2_USDT_V3_3000_WETH, expandTo18Decimals(10000)) 162 | 163 | expect(amountOut).eq(BigNumber.from('886596560223108447')) 164 | expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0xfffd8963efd1fc6a506488495d951d5263988d25'))).to.be.true 165 | expect(v3SqrtPriceX96AfterList[2].eq(BigNumber.from('0x034b624fce51aba62a4722'))).to.be.true 166 | }) 167 | 168 | it('quotes V2-V3 correctly', async () => { 169 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 170 | 'quoteExactInput(bytes,uint256)' 171 | ](UNI_V2_WETH_V3_DAI, expandTo18Decimals(10000)) 172 | 173 | expect(amountOut).eq(BigNumber.from('81108655328627859394525')) 174 | expect(v3SqrtPriceX96AfterList[1].eq(BigNumber.from('0x0518b75d40eb50192903493d'))).to.be.true 175 | }) 176 | 177 | it('quotes only V3 correctly', async () => { 178 | const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[ 179 | 'quoteExactInput(bytes,uint256)' 180 | ](UNI_V3_WETH, expandTo18Decimals(10000)) 181 | 182 | expect(amountOut.eq(BigNumber.from('32215526370828998898'))).to.be.true 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /contracts/base/OracleSlippage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '../interfaces/IOracleSlippage.sol'; 6 | 7 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 8 | import '@uniswap/v3-periphery/contracts/base/BlockTimestamp.sol'; 9 | import '@uniswap/v3-periphery/contracts/libraries/Path.sol'; 10 | import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; 11 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 12 | import '@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol'; 13 | 14 | abstract contract OracleSlippage is IOracleSlippage, PeripheryImmutableState, BlockTimestamp { 15 | using Path for bytes; 16 | 17 | /// @dev Returns the tick as of the beginning of the current block, and as of right now, for the given pool. 18 | function getBlockStartingAndCurrentTick(IUniswapV3Pool pool) 19 | internal 20 | view 21 | returns (int24 blockStartingTick, int24 currentTick) 22 | { 23 | uint16 observationIndex; 24 | uint16 observationCardinality; 25 | (, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0(); 26 | 27 | // 2 observations are needed to reliably calculate the block starting tick 28 | require(observationCardinality > 1, 'NEO'); 29 | 30 | // If the latest observation occurred in the past, then no tick-changing trades have happened in this block 31 | // therefore the tick in `slot0` is the same as at the beginning of the current block. 32 | // We don't need to check if this observation is initialized - it is guaranteed to be. 33 | (uint32 observationTimestamp, int56 tickCumulative, , ) = pool.observations(observationIndex); 34 | if (observationTimestamp != uint32(_blockTimestamp())) { 35 | blockStartingTick = currentTick; 36 | } else { 37 | uint256 prevIndex = (uint256(observationIndex) + observationCardinality - 1) % observationCardinality; 38 | (uint32 prevObservationTimestamp, int56 prevTickCumulative, , bool prevInitialized) = 39 | pool.observations(prevIndex); 40 | 41 | require(prevInitialized, 'ONI'); 42 | 43 | uint32 delta = observationTimestamp - prevObservationTimestamp; 44 | blockStartingTick = int24((tickCumulative - prevTickCumulative) / delta); 45 | } 46 | } 47 | 48 | /// @dev Virtual function to get pool addresses that can be overridden in tests. 49 | function getPoolAddress( 50 | address tokenA, 51 | address tokenB, 52 | uint24 fee 53 | ) internal view virtual returns (IUniswapV3Pool pool) { 54 | pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); 55 | } 56 | 57 | /// @dev Returns the synthetic time-weighted average tick as of secondsAgo, as well as the current tick, 58 | /// for the given path. Returned synthetic ticks always represent tokenOut/tokenIn prices, 59 | /// meaning lower ticks are worse. 60 | function getSyntheticTicks(bytes memory path, uint32 secondsAgo) 61 | internal 62 | view 63 | returns (int256 syntheticAverageTick, int256 syntheticCurrentTick) 64 | { 65 | bool lowerTicksAreWorse; 66 | 67 | uint256 numPools = path.numPools(); 68 | address previousTokenIn; 69 | for (uint256 i = 0; i < numPools; i++) { 70 | // this assumes the path is sorted in swap order 71 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 72 | IUniswapV3Pool pool = getPoolAddress(tokenIn, tokenOut, fee); 73 | 74 | // get the average and current ticks for the current pool 75 | int256 averageTick; 76 | int256 currentTick; 77 | if (secondsAgo == 0) { 78 | // we optimize for the secondsAgo == 0 case, i.e. since the beginning of the block 79 | (averageTick, currentTick) = getBlockStartingAndCurrentTick(pool); 80 | } else { 81 | (averageTick, ) = OracleLibrary.consult(address(pool), secondsAgo); 82 | (, currentTick, , , , , ) = IUniswapV3Pool(pool).slot0(); 83 | } 84 | 85 | if (i == numPools - 1) { 86 | // if we're here, this is the last pool in the path, meaning tokenOut represents the 87 | // destination token. so, if tokenIn < tokenOut, then tokenIn is token0 of the last pool, 88 | // meaning the current running ticks are going to represent tokenOut/tokenIn prices. 89 | // so, the lower these prices get, the worse of a price the swap will get 90 | lowerTicksAreWorse = tokenIn < tokenOut; 91 | } else { 92 | // if we're here, we need to iterate over the next pool in the path 93 | path = path.skipToken(); 94 | previousTokenIn = tokenIn; 95 | } 96 | 97 | // accumulate the ticks derived from the current pool into the running synthetic ticks, 98 | // ensuring that intermediate tokens "cancel out" 99 | bool add = (i == 0) || (previousTokenIn < tokenIn ? tokenIn < tokenOut : tokenOut < tokenIn); 100 | if (add) { 101 | syntheticAverageTick += averageTick; 102 | syntheticCurrentTick += currentTick; 103 | } else { 104 | syntheticAverageTick -= averageTick; 105 | syntheticCurrentTick -= currentTick; 106 | } 107 | } 108 | 109 | // flip the sign of the ticks if necessary, to ensure that the lower ticks are always worse 110 | if (!lowerTicksAreWorse) { 111 | syntheticAverageTick *= -1; 112 | syntheticCurrentTick *= -1; 113 | } 114 | } 115 | 116 | /// @dev Cast a int256 to a int24, revert on overflow or underflow 117 | function toInt24(int256 y) private pure returns (int24 z) { 118 | require((z = int24(y)) == y); 119 | } 120 | 121 | /// @dev For each passed path, fetches the synthetic time-weighted average tick as of secondsAgo, 122 | /// as well as the current tick. Then, synthetic ticks from all paths are subjected to a weighted 123 | /// average, where the weights are the fraction of the total input amount allocated to each path. 124 | /// Returned synthetic ticks always represent tokenOut/tokenIn prices, meaning lower ticks are worse. 125 | /// Paths must all start and end in the same token. 126 | function getSyntheticTicks( 127 | bytes[] memory paths, 128 | uint128[] memory amounts, 129 | uint32 secondsAgo 130 | ) internal view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) { 131 | require(paths.length == amounts.length); 132 | 133 | OracleLibrary.WeightedTickData[] memory weightedSyntheticAverageTicks = 134 | new OracleLibrary.WeightedTickData[](paths.length); 135 | OracleLibrary.WeightedTickData[] memory weightedSyntheticCurrentTicks = 136 | new OracleLibrary.WeightedTickData[](paths.length); 137 | 138 | for (uint256 i = 0; i < paths.length; i++) { 139 | (int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(paths[i], secondsAgo); 140 | weightedSyntheticAverageTicks[i].tick = toInt24(syntheticAverageTick); 141 | weightedSyntheticCurrentTicks[i].tick = toInt24(syntheticCurrentTick); 142 | weightedSyntheticAverageTicks[i].weight = amounts[i]; 143 | weightedSyntheticCurrentTicks[i].weight = amounts[i]; 144 | } 145 | 146 | averageSyntheticAverageTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticAverageTicks); 147 | averageSyntheticCurrentTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticCurrentTicks); 148 | } 149 | 150 | /// @inheritdoc IOracleSlippage 151 | function checkOracleSlippage( 152 | bytes memory path, 153 | uint24 maximumTickDivergence, 154 | uint32 secondsAgo 155 | ) external view override { 156 | (int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(path, secondsAgo); 157 | require(syntheticAverageTick - syntheticCurrentTick < maximumTickDivergence, 'TD'); 158 | } 159 | 160 | /// @inheritdoc IOracleSlippage 161 | function checkOracleSlippage( 162 | bytes[] memory paths, 163 | uint128[] memory amounts, 164 | uint24 maximumTickDivergence, 165 | uint32 secondsAgo 166 | ) external view override { 167 | (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) = 168 | getSyntheticTicks(paths, amounts, secondsAgo); 169 | require(averageSyntheticAverageTick - averageSyntheticCurrentTick < maximumTickDivergence, 'TD'); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /contracts/lens/MixedRouteQuoterV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 6 | import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; 7 | import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; 8 | import '@uniswap/v3-core/contracts/libraries/TickBitmap.sol'; 9 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 10 | import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 11 | import '@uniswap/v3-periphery/contracts/libraries/Path.sol'; 12 | import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; 13 | import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol'; 14 | import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol'; 15 | 16 | import '../base/ImmutableState.sol'; 17 | import '../interfaces/IMixedRouteQuoterV1.sol'; 18 | import '../libraries/PoolTicksCounter.sol'; 19 | import '../libraries/UniswapV2Library.sol'; 20 | 21 | /// @title Provides on chain quotes for V3, V2, and MixedRoute exact input swaps 22 | /// @notice Allows getting the expected amount out for a given swap without executing the swap 23 | /// @notice Does not support exact output swaps since using the contract balance between exactOut swaps is not supported 24 | /// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute 25 | /// the swap and check the amounts in the callback. 26 | contract MixedRouteQuoterV1 is IMixedRouteQuoterV1, IUniswapV3SwapCallback, PeripheryImmutableState { 27 | using Path for bytes; 28 | using SafeCast for uint256; 29 | using PoolTicksCounter for IUniswapV3Pool; 30 | address public immutable factoryV2; 31 | /// @dev Value to bit mask with path fee to determine if V2 or V3 route 32 | // max V3 fee: 000011110100001001000000 (24 bits) 33 | // mask: 1 << 23 = 100000000000000000000000 = decimal value 8388608 34 | uint24 private constant flagBitmask = 8388608; 35 | 36 | /// @dev Transient storage variable used to check a safety condition in exact output swaps. 37 | uint256 private amountOutCached; 38 | 39 | constructor( 40 | address _factory, 41 | address _factoryV2, 42 | address _WETH9 43 | ) PeripheryImmutableState(_factory, _WETH9) { 44 | factoryV2 = _factoryV2; 45 | } 46 | 47 | function getPool( 48 | address tokenA, 49 | address tokenB, 50 | uint24 fee 51 | ) private view returns (IUniswapV3Pool) { 52 | return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); 53 | } 54 | 55 | /// @dev Given an amountIn, fetch the reserves of the V2 pair and get the amountOut 56 | function getPairAmountOut( 57 | uint256 amountIn, 58 | address tokenIn, 59 | address tokenOut 60 | ) private view returns (uint256) { 61 | (uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves(factoryV2, tokenIn, tokenOut); 62 | return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut); 63 | } 64 | 65 | /// @inheritdoc IUniswapV3SwapCallback 66 | function uniswapV3SwapCallback( 67 | int256 amount0Delta, 68 | int256 amount1Delta, 69 | bytes memory path 70 | ) external view override { 71 | require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported 72 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 73 | CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); 74 | 75 | (bool isExactInput, uint256 amountReceived) = 76 | amount0Delta > 0 77 | ? (tokenIn < tokenOut, uint256(-amount1Delta)) 78 | : (tokenOut < tokenIn, uint256(-amount0Delta)); 79 | 80 | IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee); 81 | (uint160 v3SqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0(); 82 | 83 | if (isExactInput) { 84 | assembly { 85 | let ptr := mload(0x40) 86 | mstore(ptr, amountReceived) 87 | mstore(add(ptr, 0x20), v3SqrtPriceX96After) 88 | mstore(add(ptr, 0x40), tickAfter) 89 | revert(ptr, 0x60) 90 | } 91 | } else { 92 | /// since we don't support exactOutput, revert here 93 | revert('Exact output quote not supported'); 94 | } 95 | } 96 | 97 | /// @dev Parses a revert reason that should contain the numeric quote 98 | function parseRevertReason(bytes memory reason) 99 | private 100 | pure 101 | returns ( 102 | uint256 amount, 103 | uint160 sqrtPriceX96After, 104 | int24 tickAfter 105 | ) 106 | { 107 | if (reason.length != 0x60) { 108 | if (reason.length < 0x44) revert('Unexpected error'); 109 | assembly { 110 | reason := add(reason, 0x04) 111 | } 112 | revert(abi.decode(reason, (string))); 113 | } 114 | return abi.decode(reason, (uint256, uint160, int24)); 115 | } 116 | 117 | function handleV3Revert( 118 | bytes memory reason, 119 | IUniswapV3Pool pool, 120 | uint256 gasEstimate 121 | ) 122 | private 123 | view 124 | returns ( 125 | uint256 amount, 126 | uint160 sqrtPriceX96After, 127 | uint32 initializedTicksCrossed, 128 | uint256 129 | ) 130 | { 131 | int24 tickBefore; 132 | int24 tickAfter; 133 | (, tickBefore, , , , , ) = pool.slot0(); 134 | (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); 135 | 136 | initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter); 137 | 138 | return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); 139 | } 140 | 141 | /// @dev Fetch an exactIn quote for a V3 Pool on chain 142 | function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params) 143 | public 144 | override 145 | returns ( 146 | uint256 amountOut, 147 | uint160 sqrtPriceX96After, 148 | uint32 initializedTicksCrossed, 149 | uint256 gasEstimate 150 | ) 151 | { 152 | bool zeroForOne = params.tokenIn < params.tokenOut; 153 | IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); 154 | 155 | uint256 gasBefore = gasleft(); 156 | try 157 | pool.swap( 158 | address(this), // address(0) might cause issues with some tokens 159 | zeroForOne, 160 | params.amountIn.toInt256(), 161 | params.sqrtPriceLimitX96 == 0 162 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 163 | : params.sqrtPriceLimitX96, 164 | abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) 165 | ) 166 | {} catch (bytes memory reason) { 167 | gasEstimate = gasBefore - gasleft(); 168 | return handleV3Revert(reason, pool, gasEstimate); 169 | } 170 | } 171 | 172 | /// @dev Fetch an exactIn quote for a V2 pair on chain 173 | function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params) 174 | public 175 | view 176 | override 177 | returns (uint256 amountOut) 178 | { 179 | amountOut = getPairAmountOut(params.amountIn, params.tokenIn, params.tokenOut); 180 | } 181 | 182 | /// @dev Get the quote for an exactIn swap between an array of V2 and/or V3 pools 183 | /// @notice To encode a V2 pair within the path, use 0x800000 (hex value of 8388608) for the fee between the two token addresses 184 | function quoteExactInput(bytes memory path, uint256 amountIn) 185 | public 186 | override 187 | returns ( 188 | uint256 amountOut, 189 | uint160[] memory v3SqrtPriceX96AfterList, 190 | uint32[] memory v3InitializedTicksCrossedList, 191 | uint256 v3SwapGasEstimate 192 | ) 193 | { 194 | v3SqrtPriceX96AfterList = new uint160[](path.numPools()); 195 | v3InitializedTicksCrossedList = new uint32[](path.numPools()); 196 | 197 | uint256 i = 0; 198 | while (true) { 199 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 200 | 201 | if (fee & flagBitmask != 0) { 202 | amountIn = quoteExactInputSingleV2( 203 | QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn}) 204 | ); 205 | } else { 206 | /// the outputs of prior swaps become the inputs to subsequent ones 207 | ( 208 | uint256 _amountOut, 209 | uint160 _sqrtPriceX96After, 210 | uint32 _initializedTicksCrossed, 211 | uint256 _gasEstimate 212 | ) = 213 | quoteExactInputSingleV3( 214 | QuoteExactInputSingleV3Params({ 215 | tokenIn: tokenIn, 216 | tokenOut: tokenOut, 217 | fee: fee, 218 | amountIn: amountIn, 219 | sqrtPriceLimitX96: 0 220 | }) 221 | ); 222 | v3SqrtPriceX96AfterList[i] = _sqrtPriceX96After; 223 | v3InitializedTicksCrossedList[i] = _initializedTicksCrossed; 224 | v3SwapGasEstimate += _gasEstimate; 225 | amountIn = _amountOut; 226 | } 227 | i++; 228 | 229 | /// decide whether to continue or terminate 230 | if (path.hasMultiplePools()) { 231 | path = path.skipToken(); 232 | } else { 233 | return (amountIn, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList, v3SwapGasEstimate); 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /contracts/V3SwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; 6 | import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; 7 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 8 | import '@uniswap/v3-periphery/contracts/libraries/Path.sol'; 9 | import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; 10 | import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol'; 11 | import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; 12 | 13 | import './interfaces/IV3SwapRouter.sol'; 14 | import './base/PeripheryPaymentsWithFeeExtended.sol'; 15 | import './base/OracleSlippage.sol'; 16 | import './libraries/Constants.sol'; 17 | 18 | /// @title Uniswap V3 Swap Router 19 | /// @notice Router for stateless execution of swaps against Uniswap V3 20 | abstract contract V3SwapRouter is IV3SwapRouter, PeripheryPaymentsWithFeeExtended, OracleSlippage { 21 | using Path for bytes; 22 | using SafeCast for uint256; 23 | 24 | /// @dev Used as the placeholder value for amountInCached, because the computed amount in for an exact output swap 25 | /// can never actually be this value 26 | uint256 private constant DEFAULT_AMOUNT_IN_CACHED = type(uint256).max; 27 | 28 | /// @dev Transient storage variable used for returning the computed amount in for an exact output swap. 29 | uint256 private amountInCached = DEFAULT_AMOUNT_IN_CACHED; 30 | 31 | /// @dev Returns the pool for the given token pair and fee. The pool contract may or may not exist. 32 | function getPool( 33 | address tokenA, 34 | address tokenB, 35 | uint24 fee 36 | ) private view returns (IUniswapV3Pool) { 37 | return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); 38 | } 39 | 40 | struct SwapCallbackData { 41 | bytes path; 42 | address payer; 43 | } 44 | 45 | /// @inheritdoc IUniswapV3SwapCallback 46 | function uniswapV3SwapCallback( 47 | int256 amount0Delta, 48 | int256 amount1Delta, 49 | bytes calldata _data 50 | ) external override { 51 | require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported 52 | SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); 53 | (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); 54 | CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); 55 | 56 | (bool isExactInput, uint256 amountToPay) = 57 | amount0Delta > 0 58 | ? (tokenIn < tokenOut, uint256(amount0Delta)) 59 | : (tokenOut < tokenIn, uint256(amount1Delta)); 60 | 61 | if (isExactInput) { 62 | pay(tokenIn, data.payer, msg.sender, amountToPay); 63 | } else { 64 | // either initiate the next swap or pay 65 | if (data.path.hasMultiplePools()) { 66 | data.path = data.path.skipToken(); 67 | exactOutputInternal(amountToPay, msg.sender, 0, data); 68 | } else { 69 | amountInCached = amountToPay; 70 | // note that because exact output swaps are executed in reverse order, tokenOut is actually tokenIn 71 | pay(tokenOut, data.payer, msg.sender, amountToPay); 72 | } 73 | } 74 | } 75 | 76 | /// @dev Performs a single exact input swap 77 | function exactInputInternal( 78 | uint256 amountIn, 79 | address recipient, 80 | uint160 sqrtPriceLimitX96, 81 | SwapCallbackData memory data 82 | ) private returns (uint256 amountOut) { 83 | // find and replace recipient addresses 84 | if (recipient == Constants.MSG_SENDER) recipient = msg.sender; 85 | else if (recipient == Constants.ADDRESS_THIS) recipient = address(this); 86 | 87 | (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); 88 | 89 | bool zeroForOne = tokenIn < tokenOut; 90 | 91 | (int256 amount0, int256 amount1) = 92 | getPool(tokenIn, tokenOut, fee).swap( 93 | recipient, 94 | zeroForOne, 95 | amountIn.toInt256(), 96 | sqrtPriceLimitX96 == 0 97 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 98 | : sqrtPriceLimitX96, 99 | abi.encode(data) 100 | ); 101 | 102 | return uint256(-(zeroForOne ? amount1 : amount0)); 103 | } 104 | 105 | /// @inheritdoc IV3SwapRouter 106 | function exactInputSingle(ExactInputSingleParams memory params) 107 | external 108 | payable 109 | override 110 | returns (uint256 amountOut) 111 | { 112 | // use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract 113 | bool hasAlreadyPaid; 114 | if (params.amountIn == Constants.CONTRACT_BALANCE) { 115 | hasAlreadyPaid = true; 116 | params.amountIn = IERC20(params.tokenIn).balanceOf(address(this)); 117 | } 118 | 119 | amountOut = exactInputInternal( 120 | params.amountIn, 121 | params.recipient, 122 | params.sqrtPriceLimitX96, 123 | SwapCallbackData({ 124 | path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), 125 | payer: hasAlreadyPaid ? address(this) : msg.sender 126 | }) 127 | ); 128 | require(amountOut >= params.amountOutMinimum, 'Too little received'); 129 | } 130 | 131 | /// @inheritdoc IV3SwapRouter 132 | function exactInput(ExactInputParams memory params) external payable override returns (uint256 amountOut) { 133 | // use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract 134 | bool hasAlreadyPaid; 135 | if (params.amountIn == Constants.CONTRACT_BALANCE) { 136 | hasAlreadyPaid = true; 137 | (address tokenIn, , ) = params.path.decodeFirstPool(); 138 | params.amountIn = IERC20(tokenIn).balanceOf(address(this)); 139 | } 140 | 141 | address payer = hasAlreadyPaid ? address(this) : msg.sender; 142 | 143 | while (true) { 144 | bool hasMultiplePools = params.path.hasMultiplePools(); 145 | 146 | // the outputs of prior swaps become the inputs to subsequent ones 147 | params.amountIn = exactInputInternal( 148 | params.amountIn, 149 | hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies 150 | 0, 151 | SwapCallbackData({ 152 | path: params.path.getFirstPool(), // only the first pool in the path is necessary 153 | payer: payer 154 | }) 155 | ); 156 | 157 | // decide whether to continue or terminate 158 | if (hasMultiplePools) { 159 | payer = address(this); 160 | params.path = params.path.skipToken(); 161 | } else { 162 | amountOut = params.amountIn; 163 | break; 164 | } 165 | } 166 | 167 | require(amountOut >= params.amountOutMinimum, 'Too little received'); 168 | } 169 | 170 | /// @dev Performs a single exact output swap 171 | function exactOutputInternal( 172 | uint256 amountOut, 173 | address recipient, 174 | uint160 sqrtPriceLimitX96, 175 | SwapCallbackData memory data 176 | ) private returns (uint256 amountIn) { 177 | // find and replace recipient addresses 178 | if (recipient == Constants.MSG_SENDER) recipient = msg.sender; 179 | else if (recipient == Constants.ADDRESS_THIS) recipient = address(this); 180 | 181 | (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); 182 | 183 | bool zeroForOne = tokenIn < tokenOut; 184 | 185 | (int256 amount0Delta, int256 amount1Delta) = 186 | getPool(tokenIn, tokenOut, fee).swap( 187 | recipient, 188 | zeroForOne, 189 | -amountOut.toInt256(), 190 | sqrtPriceLimitX96 == 0 191 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 192 | : sqrtPriceLimitX96, 193 | abi.encode(data) 194 | ); 195 | 196 | uint256 amountOutReceived; 197 | (amountIn, amountOutReceived) = zeroForOne 198 | ? (uint256(amount0Delta), uint256(-amount1Delta)) 199 | : (uint256(amount1Delta), uint256(-amount0Delta)); 200 | // it's technically possible to not receive the full output amount, 201 | // so if no price limit has been specified, require this possibility away 202 | if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut); 203 | } 204 | 205 | /// @inheritdoc IV3SwapRouter 206 | function exactOutputSingle(ExactOutputSingleParams calldata params) 207 | external 208 | payable 209 | override 210 | returns (uint256 amountIn) 211 | { 212 | // avoid an SLOAD by using the swap return data 213 | amountIn = exactOutputInternal( 214 | params.amountOut, 215 | params.recipient, 216 | params.sqrtPriceLimitX96, 217 | SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender}) 218 | ); 219 | 220 | require(amountIn <= params.amountInMaximum, 'Too much requested'); 221 | // has to be reset even though we don't use it in the single hop case 222 | amountInCached = DEFAULT_AMOUNT_IN_CACHED; 223 | } 224 | 225 | /// @inheritdoc IV3SwapRouter 226 | function exactOutput(ExactOutputParams calldata params) external payable override returns (uint256 amountIn) { 227 | exactOutputInternal( 228 | params.amountOut, 229 | params.recipient, 230 | 0, 231 | SwapCallbackData({path: params.path, payer: msg.sender}) 232 | ); 233 | 234 | amountIn = amountInCached; 235 | require(amountIn <= params.amountInMaximum, 'Too much requested'); 236 | amountInCached = DEFAULT_AMOUNT_IN_CACHED; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /test/contracts/WETH9.json: -------------------------------------------------------------------------------- 1 | { 2 | "bytecode": "60606040526040805190810160405280600d81526020017f57726170706564204574686572000000000000000000000000000000000000008152506000908051906020019061004f9291906100c8565b506040805190810160405280600481526020017f57455448000000000000000000000000000000000000000000000000000000008152506001908051906020019061009b9291906100c8565b506012600260006101000a81548160ff021916908360ff16021790555034156100c357600080fd5b61016d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061010957805160ff1916838001178555610137565b82800160010185558215610137579182015b8281111561013657825182559160200191906001019061011b565b5b5090506101449190610148565b5090565b61016a91905b8082111561016657600081600090555060010161014e565b5090565b90565b610c348061017c6000396000f3006060604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b9578063095ea7b31461014757806318160ddd146101a157806323b872dd146101ca5780632e1a7d4d14610243578063313ce5671461026657806370a082311461029557806395d89b41146102e2578063a9059cbb14610370578063d0e30db0146103ca578063dd62ed3e146103d4575b6100b7610440565b005b34156100c457600080fd5b6100cc6104dd565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561010c5780820151818401526020810190506100f1565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610187600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061057b565b604051808215151515815260200191505060405180910390f35b34156101ac57600080fd5b6101b461066d565b6040518082815260200191505060405180910390f35b34156101d557600080fd5b610229600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061068c565b604051808215151515815260200191505060405180910390f35b341561024e57600080fd5b61026460048080359060200190919050506109d9565b005b341561027157600080fd5b610279610b05565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a057600080fd5b6102cc600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610b18565b6040518082815260200191505060405180910390f35b34156102ed57600080fd5b6102f5610b30565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561033557808201518184015260208101905061031a565b50505050905090810190601f1680156103625780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037b57600080fd5b6103b0600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610bce565b604051808215151515815260200191505060405180910390f35b6103d2610440565b005b34156103df57600080fd5b61042a600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610be3565b6040518082815260200191505060405180910390f35b34600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055503373ffffffffffffffffffffffffffffffffffffffff167fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c346040518082815260200191505060405180910390a2565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156105735780601f1061054857610100808354040283529160200191610573565b820191906000526020600020905b81548152906001019060200180831161055657829003601f168201915b505050505081565b600081600460003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b600081600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101515156106dc57600080fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141580156107b457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b156108cf5781600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015151561084457600080fd5b81600460008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055505b81600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410151515610a2757600080fd5b80600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610ab457600080fd5b3373ffffffffffffffffffffffffffffffffffffffff167f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65826040518082815260200191505060405180910390a250565b600260009054906101000a900460ff1681565b60036020528060005260406000206000915090505481565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610bc65780601f10610b9b57610100808354040283529160200191610bc6565b820191906000526020600020905b815481529060010190602001808311610ba957829003601f168201915b505050505081565b6000610bdb33848461068c565b905092915050565b60046020528160005260406000206020528060005260406000206000915091505054815600a165627a7a72305820deb4c2ccab3c2fdca32ab3f46728389c2fe2c165d5fafa07661e4e004f6c344a0029", 3 | "abi": [ 4 | { 5 | "constant": true, 6 | "inputs": [], 7 | "name": "name", 8 | "outputs": [{ "name": "", "type": "string" }], 9 | "payable": false, 10 | "stateMutability": "view", 11 | "type": "function" 12 | }, 13 | { 14 | "constant": false, 15 | "inputs": [ 16 | { "name": "guy", "type": "address" }, 17 | { "name": "wad", "type": "uint256" } 18 | ], 19 | "name": "approve", 20 | "outputs": [{ "name": "", "type": "bool" }], 21 | "payable": false, 22 | "stateMutability": "nonpayable", 23 | "type": "function" 24 | }, 25 | { 26 | "constant": true, 27 | "inputs": [], 28 | "name": "totalSupply", 29 | "outputs": [{ "name": "", "type": "uint256" }], 30 | "payable": false, 31 | "stateMutability": "view", 32 | "type": "function" 33 | }, 34 | { 35 | "constant": false, 36 | "inputs": [ 37 | { "name": "src", "type": "address" }, 38 | { "name": "dst", "type": "address" }, 39 | { "name": "wad", "type": "uint256" } 40 | ], 41 | "name": "transferFrom", 42 | "outputs": [{ "name": "", "type": "bool" }], 43 | "payable": false, 44 | "stateMutability": "nonpayable", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": false, 49 | "inputs": [{ "name": "wad", "type": "uint256" }], 50 | "name": "withdraw", 51 | "outputs": [], 52 | "payable": false, 53 | "stateMutability": "nonpayable", 54 | "type": "function" 55 | }, 56 | { 57 | "constant": true, 58 | "inputs": [], 59 | "name": "decimals", 60 | "outputs": [{ "name": "", "type": "uint8" }], 61 | "payable": false, 62 | "stateMutability": "view", 63 | "type": "function" 64 | }, 65 | { 66 | "constant": true, 67 | "inputs": [{ "name": "", "type": "address" }], 68 | "name": "balanceOf", 69 | "outputs": [{ "name": "", "type": "uint256" }], 70 | "payable": false, 71 | "stateMutability": "view", 72 | "type": "function" 73 | }, 74 | { 75 | "constant": true, 76 | "inputs": [], 77 | "name": "symbol", 78 | "outputs": [{ "name": "", "type": "string" }], 79 | "payable": false, 80 | "stateMutability": "view", 81 | "type": "function" 82 | }, 83 | { 84 | "constant": false, 85 | "inputs": [ 86 | { "name": "dst", "type": "address" }, 87 | { "name": "wad", "type": "uint256" } 88 | ], 89 | "name": "transfer", 90 | "outputs": [{ "name": "", "type": "bool" }], 91 | "payable": false, 92 | "stateMutability": "nonpayable", 93 | "type": "function" 94 | }, 95 | { 96 | "constant": false, 97 | "inputs": [], 98 | "name": "deposit", 99 | "outputs": [], 100 | "payable": true, 101 | "stateMutability": "payable", 102 | "type": "function" 103 | }, 104 | { 105 | "constant": true, 106 | "inputs": [ 107 | { "name": "", "type": "address" }, 108 | { "name": "", "type": "address" } 109 | ], 110 | "name": "allowance", 111 | "outputs": [{ "name": "", "type": "uint256" }], 112 | "payable": false, 113 | "stateMutability": "view", 114 | "type": "function" 115 | }, 116 | { "payable": true, "stateMutability": "payable", "type": "fallback" }, 117 | { 118 | "anonymous": false, 119 | "inputs": [ 120 | { "indexed": true, "name": "src", "type": "address" }, 121 | { "indexed": true, "name": "guy", "type": "address" }, 122 | { "indexed": false, "name": "wad", "type": "uint256" } 123 | ], 124 | "name": "Approval", 125 | "type": "event" 126 | }, 127 | { 128 | "anonymous": false, 129 | "inputs": [ 130 | { "indexed": true, "name": "src", "type": "address" }, 131 | { "indexed": true, "name": "dst", "type": "address" }, 132 | { "indexed": false, "name": "wad", "type": "uint256" } 133 | ], 134 | "name": "Transfer", 135 | "type": "event" 136 | }, 137 | { 138 | "anonymous": false, 139 | "inputs": [ 140 | { "indexed": true, "name": "dst", "type": "address" }, 141 | { "indexed": false, "name": "wad", "type": "uint256" } 142 | ], 143 | "name": "Deposit", 144 | "type": "event" 145 | }, 146 | { 147 | "anonymous": false, 148 | "inputs": [ 149 | { "indexed": true, "name": "src", "type": "address" }, 150 | { "indexed": false, "name": "wad", "type": "uint256" } 151 | ], 152 | "name": "Withdrawal", 153 | "type": "event" 154 | } 155 | ] 156 | } 157 | -------------------------------------------------------------------------------- /contracts/lens/QuoterV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity =0.7.6; 3 | pragma abicoder v2; 4 | 5 | import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol'; 6 | import '@uniswap/v3-core/contracts/libraries/SafeCast.sol'; 7 | import '@uniswap/v3-core/contracts/libraries/TickMath.sol'; 8 | import '@uniswap/v3-core/contracts/libraries/TickBitmap.sol'; 9 | import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol'; 10 | import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol'; 11 | import '@uniswap/v3-periphery/contracts/libraries/Path.sol'; 12 | import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol'; 13 | import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol'; 14 | 15 | import '../interfaces/IQuoterV2.sol'; 16 | import '../libraries/PoolTicksCounter.sol'; 17 | 18 | /// @title Provides quotes for swaps 19 | /// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap 20 | /// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute 21 | /// the swap and check the amounts in the callback. 22 | contract QuoterV2 is IQuoterV2, IUniswapV3SwapCallback, PeripheryImmutableState { 23 | using Path for bytes; 24 | using SafeCast for uint256; 25 | using PoolTicksCounter for IUniswapV3Pool; 26 | 27 | /// @dev Transient storage variable used to check a safety condition in exact output swaps. 28 | uint256 private amountOutCached; 29 | 30 | constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {} 31 | 32 | function getPool( 33 | address tokenA, 34 | address tokenB, 35 | uint24 fee 36 | ) private view returns (IUniswapV3Pool) { 37 | return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee))); 38 | } 39 | 40 | /// @inheritdoc IUniswapV3SwapCallback 41 | function uniswapV3SwapCallback( 42 | int256 amount0Delta, 43 | int256 amount1Delta, 44 | bytes memory path 45 | ) external view override { 46 | require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported 47 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 48 | CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); 49 | 50 | (bool isExactInput, uint256 amountToPay, uint256 amountReceived) = 51 | amount0Delta > 0 52 | ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta)) 53 | : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta)); 54 | 55 | IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee); 56 | (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0(); 57 | 58 | if (isExactInput) { 59 | assembly { 60 | let ptr := mload(0x40) 61 | mstore(ptr, amountReceived) 62 | mstore(add(ptr, 0x20), sqrtPriceX96After) 63 | mstore(add(ptr, 0x40), tickAfter) 64 | revert(ptr, 96) 65 | } 66 | } else { 67 | // if the cache has been populated, ensure that the full output amount has been received 68 | if (amountOutCached != 0) require(amountReceived == amountOutCached); 69 | assembly { 70 | let ptr := mload(0x40) 71 | mstore(ptr, amountToPay) 72 | mstore(add(ptr, 0x20), sqrtPriceX96After) 73 | mstore(add(ptr, 0x40), tickAfter) 74 | revert(ptr, 96) 75 | } 76 | } 77 | } 78 | 79 | /// @dev Parses a revert reason that should contain the numeric quote 80 | function parseRevertReason(bytes memory reason) 81 | private 82 | pure 83 | returns ( 84 | uint256 amount, 85 | uint160 sqrtPriceX96After, 86 | int24 tickAfter 87 | ) 88 | { 89 | if (reason.length != 96) { 90 | if (reason.length < 68) revert('Unexpected error'); 91 | assembly { 92 | reason := add(reason, 0x04) 93 | } 94 | revert(abi.decode(reason, (string))); 95 | } 96 | return abi.decode(reason, (uint256, uint160, int24)); 97 | } 98 | 99 | function handleRevert( 100 | bytes memory reason, 101 | IUniswapV3Pool pool, 102 | uint256 gasEstimate 103 | ) 104 | private 105 | view 106 | returns ( 107 | uint256 amount, 108 | uint160 sqrtPriceX96After, 109 | uint32 initializedTicksCrossed, 110 | uint256 111 | ) 112 | { 113 | int24 tickBefore; 114 | int24 tickAfter; 115 | (, tickBefore, , , , , ) = pool.slot0(); 116 | (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); 117 | 118 | initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter); 119 | 120 | return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); 121 | } 122 | 123 | function quoteExactInputSingle(QuoteExactInputSingleParams memory params) 124 | public 125 | override 126 | returns ( 127 | uint256 amountOut, 128 | uint160 sqrtPriceX96After, 129 | uint32 initializedTicksCrossed, 130 | uint256 gasEstimate 131 | ) 132 | { 133 | bool zeroForOne = params.tokenIn < params.tokenOut; 134 | IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); 135 | 136 | uint256 gasBefore = gasleft(); 137 | try 138 | pool.swap( 139 | address(this), // address(0) might cause issues with some tokens 140 | zeroForOne, 141 | params.amountIn.toInt256(), 142 | params.sqrtPriceLimitX96 == 0 143 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 144 | : params.sqrtPriceLimitX96, 145 | abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) 146 | ) 147 | {} catch (bytes memory reason) { 148 | gasEstimate = gasBefore - gasleft(); 149 | return handleRevert(reason, pool, gasEstimate); 150 | } 151 | } 152 | 153 | function quoteExactInput(bytes memory path, uint256 amountIn) 154 | public 155 | override 156 | returns ( 157 | uint256 amountOut, 158 | uint160[] memory sqrtPriceX96AfterList, 159 | uint32[] memory initializedTicksCrossedList, 160 | uint256 gasEstimate 161 | ) 162 | { 163 | sqrtPriceX96AfterList = new uint160[](path.numPools()); 164 | initializedTicksCrossedList = new uint32[](path.numPools()); 165 | 166 | uint256 i = 0; 167 | while (true) { 168 | (address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool(); 169 | 170 | // the outputs of prior swaps become the inputs to subsequent ones 171 | (uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) = 172 | quoteExactInputSingle( 173 | QuoteExactInputSingleParams({ 174 | tokenIn: tokenIn, 175 | tokenOut: tokenOut, 176 | fee: fee, 177 | amountIn: amountIn, 178 | sqrtPriceLimitX96: 0 179 | }) 180 | ); 181 | 182 | sqrtPriceX96AfterList[i] = _sqrtPriceX96After; 183 | initializedTicksCrossedList[i] = _initializedTicksCrossed; 184 | amountIn = _amountOut; 185 | gasEstimate += _gasEstimate; 186 | i++; 187 | 188 | // decide whether to continue or terminate 189 | if (path.hasMultiplePools()) { 190 | path = path.skipToken(); 191 | } else { 192 | return (amountIn, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate); 193 | } 194 | } 195 | } 196 | 197 | function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) 198 | public 199 | override 200 | returns ( 201 | uint256 amountIn, 202 | uint160 sqrtPriceX96After, 203 | uint32 initializedTicksCrossed, 204 | uint256 gasEstimate 205 | ) 206 | { 207 | bool zeroForOne = params.tokenIn < params.tokenOut; 208 | IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); 209 | 210 | // if no price limit has been specified, cache the output amount for comparison in the swap callback 211 | if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.amount; 212 | uint256 gasBefore = gasleft(); 213 | try 214 | pool.swap( 215 | address(this), // address(0) might cause issues with some tokens 216 | zeroForOne, 217 | -params.amount.toInt256(), 218 | params.sqrtPriceLimitX96 == 0 219 | ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) 220 | : params.sqrtPriceLimitX96, 221 | abi.encodePacked(params.tokenOut, params.fee, params.tokenIn) 222 | ) 223 | {} catch (bytes memory reason) { 224 | gasEstimate = gasBefore - gasleft(); 225 | if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache 226 | return handleRevert(reason, pool, gasEstimate); 227 | } 228 | } 229 | 230 | function quoteExactOutput(bytes memory path, uint256 amountOut) 231 | public 232 | override 233 | returns ( 234 | uint256 amountIn, 235 | uint160[] memory sqrtPriceX96AfterList, 236 | uint32[] memory initializedTicksCrossedList, 237 | uint256 gasEstimate 238 | ) 239 | { 240 | sqrtPriceX96AfterList = new uint160[](path.numPools()); 241 | initializedTicksCrossedList = new uint32[](path.numPools()); 242 | 243 | uint256 i = 0; 244 | while (true) { 245 | (address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool(); 246 | 247 | // the inputs of prior swaps become the outputs of subsequent ones 248 | (uint256 _amountIn, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) = 249 | quoteExactOutputSingle( 250 | QuoteExactOutputSingleParams({ 251 | tokenIn: tokenIn, 252 | tokenOut: tokenOut, 253 | amount: amountOut, 254 | fee: fee, 255 | sqrtPriceLimitX96: 0 256 | }) 257 | ); 258 | 259 | sqrtPriceX96AfterList[i] = _sqrtPriceX96After; 260 | initializedTicksCrossedList[i] = _initializedTicksCrossed; 261 | amountOut = _amountIn; 262 | gasEstimate += _gasEstimate; 263 | i++; 264 | 265 | // decide whether to continue or terminate 266 | if (path.hasMultiplePools()) { 267 | path = path.skipToken(); 268 | } else { 269 | return (amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate); 270 | } 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /test/PoolTicksCounter.spec.ts: -------------------------------------------------------------------------------- 1 | import { waffle, ethers, artifacts } from 'hardhat' 2 | 3 | import { expect } from './shared/expect' 4 | 5 | import { PoolTicksCounterTest } from '../typechain' 6 | import { deployMockContract, Fixture, MockContract } from 'ethereum-waffle' 7 | import { Artifact } from 'hardhat/types' 8 | 9 | describe('PoolTicksCounter', () => { 10 | const TICK_SPACINGS = [200, 60, 10] 11 | 12 | TICK_SPACINGS.forEach((TICK_SPACING) => { 13 | let PoolTicksCounter: PoolTicksCounterTest 14 | let pool: MockContract 15 | let PoolAbi: Artifact 16 | 17 | // Bit index to tick 18 | const bitIdxToTick = (idx: number, page = 0) => { 19 | return idx * TICK_SPACING + page * 256 * TICK_SPACING 20 | } 21 | 22 | before(async () => { 23 | const wallets = await (ethers as any).getSigners() 24 | PoolAbi = await artifacts.readArtifact('IUniswapV3Pool') 25 | const poolTicksHelperFactory = await ethers.getContractFactory('PoolTicksCounterTest') 26 | PoolTicksCounter = (await poolTicksHelperFactory.deploy()) as PoolTicksCounterTest 27 | pool = await deployMockContract(wallets[0], PoolAbi.abi) 28 | await pool.mock.tickSpacing.returns(TICK_SPACING) 29 | }) 30 | 31 | describe(`[Tick Spacing: ${TICK_SPACING}]: tick after is bigger`, async () => { 32 | it('same tick initialized', async () => { 33 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100 34 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 35 | pool.address, 36 | bitIdxToTick(2), 37 | bitIdxToTick(2) 38 | ) 39 | expect(result).to.be.eq(1) 40 | }) 41 | 42 | it('same tick not-initialized', async () => { 43 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100 44 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 45 | pool.address, 46 | bitIdxToTick(1), 47 | bitIdxToTick(1) 48 | ) 49 | expect(result).to.be.eq(0) 50 | }) 51 | 52 | it('same page', async () => { 53 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100 54 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 55 | pool.address, 56 | bitIdxToTick(0), 57 | bitIdxToTick(255) 58 | ) 59 | expect(result).to.be.eq(2) 60 | }) 61 | 62 | it('multiple pages', async () => { 63 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100 64 | await pool.mock.tickBitmap.withArgs(1).returns(0b1101) // 1101 65 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 66 | pool.address, 67 | bitIdxToTick(0), 68 | bitIdxToTick(255, 1) 69 | ) 70 | expect(result).to.be.eq(5) 71 | }) 72 | 73 | it('counts all ticks in a page except ending tick', async () => { 74 | await pool.mock.tickBitmap.withArgs(0).returns(ethers.constants.MaxUint256) 75 | await pool.mock.tickBitmap.withArgs(1).returns(0x0) 76 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 77 | pool.address, 78 | bitIdxToTick(0), 79 | bitIdxToTick(255, 1) 80 | ) 81 | expect(result).to.be.eq(255) 82 | }) 83 | 84 | it('counts ticks to left of start and right of end on same page', async () => { 85 | await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111) 86 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 87 | pool.address, 88 | bitIdxToTick(8), 89 | bitIdxToTick(255) 90 | ) 91 | expect(result).to.be.eq(4) 92 | }) 93 | 94 | it('counts ticks to left of start and right of end across on multiple pages', async () => { 95 | await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111) 96 | await pool.mock.tickBitmap.withArgs(1).returns(0b1111000100001111) 97 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 98 | pool.address, 99 | bitIdxToTick(8), 100 | bitIdxToTick(8, 1) 101 | ) 102 | expect(result).to.be.eq(9) 103 | }) 104 | 105 | it('counts ticks when before and after are initialized on same page', async () => { 106 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 107 | const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 108 | pool.address, 109 | bitIdxToTick(2), 110 | bitIdxToTick(255) 111 | ) 112 | expect(startingTickInit).to.be.eq(5) 113 | const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 114 | pool.address, 115 | bitIdxToTick(0), 116 | bitIdxToTick(3) 117 | ) 118 | expect(endingTickInit).to.be.eq(2) 119 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 120 | pool.address, 121 | bitIdxToTick(2), 122 | bitIdxToTick(5) 123 | ) 124 | expect(bothInit).to.be.eq(3) 125 | }) 126 | 127 | it('counts ticks when before and after are initialized on multiple page', async () => { 128 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 129 | await pool.mock.tickBitmap.withArgs(1).returns(0b11111100) 130 | const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 131 | pool.address, 132 | bitIdxToTick(2), 133 | bitIdxToTick(255) 134 | ) 135 | expect(startingTickInit).to.be.eq(5) 136 | const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 137 | pool.address, 138 | bitIdxToTick(0), 139 | bitIdxToTick(3, 1) 140 | ) 141 | expect(endingTickInit).to.be.eq(8) 142 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 143 | pool.address, 144 | bitIdxToTick(2), 145 | bitIdxToTick(5, 1) 146 | ) 147 | expect(bothInit).to.be.eq(9) 148 | }) 149 | 150 | it('counts ticks with lots of pages', async () => { 151 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 152 | await pool.mock.tickBitmap.withArgs(1).returns(0b11111111) 153 | await pool.mock.tickBitmap.withArgs(2).returns(0x0) 154 | await pool.mock.tickBitmap.withArgs(3).returns(0x0) 155 | await pool.mock.tickBitmap.withArgs(4).returns(0b11111100) 156 | 157 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 158 | pool.address, 159 | bitIdxToTick(4), 160 | bitIdxToTick(5, 4) 161 | ) 162 | expect(bothInit).to.be.eq(15) 163 | }) 164 | }) 165 | 166 | describe(`[Tick Spacing: ${TICK_SPACING}]: tick after is smaller`, async () => { 167 | it('same page', async () => { 168 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) 169 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 170 | pool.address, 171 | bitIdxToTick(255), 172 | bitIdxToTick(0) 173 | ) 174 | expect(result).to.be.eq(2) 175 | }) 176 | 177 | it('multiple pages', async () => { 178 | await pool.mock.tickBitmap.withArgs(0).returns(0b1100) 179 | await pool.mock.tickBitmap.withArgs(-1).returns(0b1100) 180 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 181 | pool.address, 182 | bitIdxToTick(255), 183 | bitIdxToTick(0, -1) 184 | ) 185 | expect(result).to.be.eq(4) 186 | }) 187 | 188 | it('counts all ticks in a page', async () => { 189 | await pool.mock.tickBitmap.withArgs(0).returns(ethers.constants.MaxUint256) 190 | await pool.mock.tickBitmap.withArgs(-1).returns(0x0) 191 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 192 | pool.address, 193 | bitIdxToTick(255), 194 | bitIdxToTick(0, -1) 195 | ) 196 | expect(result).to.be.eq(256) 197 | }) 198 | 199 | it('counts ticks to right of start and left of end on same page', async () => { 200 | await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111) 201 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 202 | pool.address, 203 | bitIdxToTick(15), 204 | bitIdxToTick(2) 205 | ) 206 | expect(result).to.be.eq(6) 207 | }) 208 | 209 | it('counts ticks to right of start and left of end on multiple pages', async () => { 210 | await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111) 211 | await pool.mock.tickBitmap.withArgs(-1).returns(0b1111000100001111) 212 | const result = await PoolTicksCounter.countInitializedTicksCrossed( 213 | pool.address, 214 | bitIdxToTick(8), 215 | bitIdxToTick(8, -1) 216 | ) 217 | expect(result).to.be.eq(9) 218 | }) 219 | 220 | it('counts ticks when before and after are initialized on same page', async () => { 221 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 222 | const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 223 | pool.address, 224 | bitIdxToTick(3), 225 | bitIdxToTick(0) 226 | ) 227 | expect(startingTickInit).to.be.eq(2) 228 | const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 229 | pool.address, 230 | bitIdxToTick(255), 231 | bitIdxToTick(2) 232 | ) 233 | expect(endingTickInit).to.be.eq(5) 234 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 235 | pool.address, 236 | bitIdxToTick(5), 237 | bitIdxToTick(2) 238 | ) 239 | expect(bothInit).to.be.eq(3) 240 | }) 241 | 242 | it('counts ticks when before and after are initialized on multiple page', async () => { 243 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 244 | await pool.mock.tickBitmap.withArgs(-1).returns(0b11111100) 245 | const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 246 | pool.address, 247 | bitIdxToTick(2), 248 | bitIdxToTick(3, -1) 249 | ) 250 | expect(startingTickInit).to.be.eq(5) 251 | const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed( 252 | pool.address, 253 | bitIdxToTick(5), 254 | bitIdxToTick(255, -1) 255 | ) 256 | expect(endingTickInit).to.be.eq(4) 257 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 258 | pool.address, 259 | bitIdxToTick(2), 260 | bitIdxToTick(5, -1) 261 | ) 262 | expect(bothInit).to.be.eq(3) 263 | }) 264 | 265 | it('counts ticks with lots of pages', async () => { 266 | await pool.mock.tickBitmap.withArgs(0).returns(0b11111100) 267 | await pool.mock.tickBitmap.withArgs(-1).returns(0xff) 268 | await pool.mock.tickBitmap.withArgs(-2).returns(0x0) 269 | await pool.mock.tickBitmap.withArgs(-3).returns(0x0) 270 | await pool.mock.tickBitmap.withArgs(-4).returns(0b11111100) 271 | const bothInit = await PoolTicksCounter.countInitializedTicksCrossed( 272 | pool.address, 273 | bitIdxToTick(3), 274 | bitIdxToTick(6, -4) 275 | ) 276 | expect(bothInit).to.be.eq(11) 277 | }) 278 | }) 279 | }) 280 | }) 281 | -------------------------------------------------------------------------------- /test/ApproveAndCall.spec.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbiCoder } from '@ethersproject/abi' 2 | import { Fixture } from 'ethereum-waffle' 3 | import { constants, Contract, ContractTransaction, Wallet } from 'ethers' 4 | import { solidityPack } from 'ethers/lib/utils' 5 | import { ethers, waffle } from 'hardhat' 6 | import { MockTimeSwapRouter02, TestERC20 } from '../typechain' 7 | import completeFixture from './shared/completeFixture' 8 | import { ADDRESS_THIS, FeeAmount, TICK_SPACINGS } from './shared/constants' 9 | import { encodePriceSqrt } from './shared/encodePriceSqrt' 10 | import { expect } from './shared/expect' 11 | import { encodePath } from './shared/path' 12 | import { getMaxTick, getMinTick } from './shared/ticks' 13 | 14 | enum ApprovalType { 15 | NOT_REQUIRED, 16 | MAX, 17 | MAX_MINUS_ONE, 18 | ZERO_THEN_MAX, 19 | ZERO_THEN_MAX_MINUS_ONE, 20 | } 21 | 22 | describe('ApproveAndCall', function () { 23 | this.timeout(40000) 24 | let wallet: Wallet 25 | let trader: Wallet 26 | 27 | const swapRouterFixture: Fixture<{ 28 | factory: Contract 29 | router: MockTimeSwapRouter02 30 | nft: Contract 31 | tokens: [TestERC20, TestERC20, TestERC20] 32 | }> = async (wallets, provider) => { 33 | const { factory, router, tokens, nft } = await completeFixture(wallets, provider) 34 | 35 | // approve & fund wallets 36 | for (const token of tokens) { 37 | await token.approve(nft.address, constants.MaxUint256) 38 | } 39 | 40 | return { 41 | factory, 42 | router, 43 | tokens, 44 | nft, 45 | } 46 | } 47 | 48 | let factory: Contract 49 | let router: MockTimeSwapRouter02 50 | let nft: Contract 51 | let tokens: [TestERC20, TestERC20, TestERC20] 52 | 53 | let loadFixture: ReturnType 54 | 55 | function encodeSweepToken(token: string, amount: number) { 56 | const functionSignature = 'sweepToken(address,uint256)' 57 | return solidityPack( 58 | ['bytes4', 'bytes'], 59 | [router.interface.getSighash(functionSignature), defaultAbiCoder.encode(['address', 'uint256'], [token, amount])] 60 | ) 61 | } 62 | 63 | before('create fixture loader', async () => { 64 | ;[wallet, trader] = await (ethers as any).getSigners() 65 | loadFixture = waffle.createFixtureLoader([wallet, trader]) 66 | }) 67 | 68 | beforeEach('load fixture', async () => { 69 | ;({ factory, router, tokens, nft } = await loadFixture(swapRouterFixture)) 70 | }) 71 | 72 | describe('swap and add', () => { 73 | async function createPool(tokenAddressA: string, tokenAddressB: string) { 74 | if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase()) 75 | [tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA] 76 | 77 | await nft.createAndInitializePoolIfNecessary( 78 | tokenAddressA, 79 | tokenAddressB, 80 | FeeAmount.MEDIUM, 81 | encodePriceSqrt(1, 1) 82 | ) 83 | 84 | const liquidityParams = { 85 | token0: tokenAddressA, 86 | token1: tokenAddressB, 87 | fee: FeeAmount.MEDIUM, 88 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 89 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 90 | recipient: wallet.address, 91 | amount0Desired: 1000000, 92 | amount1Desired: 1000000, 93 | amount0Min: 0, 94 | amount1Min: 0, 95 | deadline: 2 ** 32, 96 | } 97 | 98 | return nft.mint(liquidityParams) 99 | } 100 | 101 | describe('approvals', () => { 102 | it('#approveMax', async () => { 103 | let approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123) 104 | expect(approvalType).to.be.eq(ApprovalType.MAX) 105 | 106 | await router.approveMax(tokens[0].address) 107 | 108 | approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123) 109 | expect(approvalType).to.be.eq(ApprovalType.NOT_REQUIRED) 110 | }) 111 | 112 | it('#approveMax', async () => { 113 | await router.approveMax(tokens[0].address) 114 | }) 115 | 116 | it('#approveMaxMinusOne', async () => { 117 | await router.approveMaxMinusOne(tokens[0].address) 118 | }) 119 | 120 | describe('#approveZeroThenMax', async () => { 121 | it('from 0', async () => { 122 | await router.approveZeroThenMax(tokens[0].address) 123 | }) 124 | it('from max', async () => { 125 | await router.approveMax(tokens[0].address) 126 | await router.approveZeroThenMax(tokens[0].address) 127 | }) 128 | }) 129 | 130 | describe('#approveZeroThenMax', async () => { 131 | it('from 0', async () => { 132 | await router.approveZeroThenMaxMinusOne(tokens[0].address) 133 | }) 134 | it('from max', async () => { 135 | await router.approveMax(tokens[0].address) 136 | await router.approveZeroThenMaxMinusOne(tokens[0].address) 137 | }) 138 | }) 139 | }) 140 | 141 | it('#mint and #increaseLiquidity', async () => { 142 | await createPool(tokens[0].address, tokens[1].address) 143 | const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM) 144 | 145 | // approve in advance 146 | await router.approveMax(tokens[0].address) 147 | await router.approveMax(tokens[1].address) 148 | 149 | // send dummy amount of tokens to the pair in advance 150 | const amount = 1000 151 | await tokens[0].transfer(router.address, amount) 152 | await tokens[1].transfer(router.address, amount) 153 | expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(amount) 154 | expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(amount) 155 | 156 | let poolBalance0Before = await tokens[0].balanceOf(pool) 157 | let poolBalance1Before = await tokens[1].balanceOf(pool) 158 | 159 | // perform the mint 160 | await router.mint({ 161 | token0: tokens[0].address, 162 | token1: tokens[1].address, 163 | fee: FeeAmount.MEDIUM, 164 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 165 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 166 | recipient: trader.address, 167 | amount0Min: 0, 168 | amount1Min: 0, 169 | }) 170 | 171 | expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0) 172 | expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0) 173 | expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount) 174 | expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount) 175 | 176 | expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1) 177 | 178 | // send more tokens 179 | await tokens[0].transfer(router.address, amount) 180 | await tokens[1].transfer(router.address, amount) 181 | 182 | // perform the increaseLiquidity 183 | await router.increaseLiquidity({ 184 | token0: tokens[0].address, 185 | token1: tokens[1].address, 186 | tokenId: 2, 187 | amount0Min: 0, 188 | amount1Min: 0, 189 | }) 190 | 191 | expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0) 192 | expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0) 193 | expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount * 2) 194 | expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount * 2) 195 | 196 | expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1) 197 | }) 198 | 199 | describe('single-asset add', () => { 200 | beforeEach('create 0-1 pool', async () => { 201 | await createPool(tokens[0].address, tokens[1].address) 202 | }) 203 | 204 | async function singleAssetAddExactInput( 205 | tokenIn: string, 206 | tokenOut: string, 207 | amountIn: number, 208 | amountOutMinimum: number 209 | ): Promise { 210 | // encode the exact input swap 211 | const params = { 212 | path: encodePath([tokenIn, tokenOut], [FeeAmount.MEDIUM]), 213 | recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller 214 | amountIn, 215 | amountOutMinimum, 216 | } 217 | // ensure that the swap fails if the limit is any tighter 218 | const amountOut = await router.connect(trader).callStatic.exactInput(params) 219 | expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) 220 | const data = [router.interface.encodeFunctionData('exactInput', [params])] 221 | 222 | // encode the pull (we take the same as the amountOutMinimum, assuming a 50/50 range) 223 | data.push(router.interface.encodeFunctionData('pull', [tokenIn, amountOutMinimum])) 224 | 225 | // encode the approves 226 | data.push(router.interface.encodeFunctionData('approveMax', [tokenIn])) 227 | data.push(router.interface.encodeFunctionData('approveMax', [tokenOut])) 228 | 229 | // encode the add liquidity 230 | const [token0, token1] = 231 | tokenIn.toLowerCase() < tokenOut.toLowerCase() ? [tokenIn, tokenOut] : [tokenOut, tokenIn] 232 | const liquidityParams = { 233 | token0, 234 | token1, 235 | fee: FeeAmount.MEDIUM, 236 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 237 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 238 | recipient: trader.address, 239 | amount0Desired: amountOutMinimum, 240 | amount1Desired: amountOutMinimum, 241 | amount0Min: 0, 242 | amount1Min: 0, 243 | deadline: 2 ** 32, 244 | } 245 | data.push( 246 | router.interface.encodeFunctionData('callPositionManager', [ 247 | nft.interface.encodeFunctionData('mint', [liquidityParams]), 248 | ]) 249 | ) 250 | 251 | // encode the sweeps 252 | data.push(encodeSweepToken(tokenIn, 0)) 253 | data.push(encodeSweepToken(tokenOut, 0)) 254 | 255 | return router.connect(trader)['multicall(bytes[])'](data) 256 | } 257 | 258 | it('0 -> 1', async () => { 259 | const amountIn = 1000 260 | const amountOutMinimum = 996 261 | 262 | // prep for the swap + add by sending tokens 263 | await tokens[0].transfer(trader.address, amountIn + amountOutMinimum) 264 | await tokens[0].connect(trader).approve(router.address, amountIn + amountOutMinimum) 265 | 266 | const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address) 267 | const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address) 268 | expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn + amountOutMinimum) 269 | expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0) 270 | 271 | const traderNFTBalanceBefore = await nft.balanceOf(trader.address) 272 | expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0) 273 | 274 | await singleAssetAddExactInput(tokens[0].address, tokens[1].address, amountIn, amountOutMinimum) 275 | 276 | const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address) 277 | const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address) 278 | expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0) 279 | expect(traderToken1BalanceAfter.toNumber()).to.be.eq(1) // dust 280 | 281 | const traderNFTBalanceAfter = await nft.balanceOf(trader.address) 282 | expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1) 283 | }) 284 | }) 285 | 286 | describe('any-asset add', () => { 287 | beforeEach('create 0-1, 0-2, and 1-2 pools pools', async () => { 288 | await createPool(tokens[0].address, tokens[1].address) 289 | await createPool(tokens[0].address, tokens[2].address) 290 | await createPool(tokens[1].address, tokens[2].address) 291 | }) 292 | 293 | async function anyAssetAddExactInput( 294 | tokenStart: string, 295 | tokenA: string, 296 | tokenB: string, 297 | amountIn: number, 298 | amountOutMinimum: number 299 | ): Promise { 300 | // encode the exact input swaps 301 | let params = { 302 | path: encodePath([tokenStart, tokenA], [FeeAmount.MEDIUM]), 303 | recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller 304 | amountIn, 305 | amountOutMinimum, 306 | } 307 | // ensure that the swap fails if the limit is any tighter 308 | let amountOut = await router.connect(trader).callStatic.exactInput(params) 309 | expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) 310 | let data = [router.interface.encodeFunctionData('exactInput', [params])] 311 | 312 | // encode the exact input swaps 313 | params = { 314 | path: encodePath([tokenStart, tokenB], [FeeAmount.MEDIUM]), 315 | recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller 316 | amountIn, 317 | amountOutMinimum, 318 | } 319 | // ensure that the swap fails if the limit is any tighter 320 | amountOut = await router.connect(trader).callStatic.exactInput(params) 321 | expect(amountOut.toNumber()).to.be.eq(amountOutMinimum) 322 | data.push(router.interface.encodeFunctionData('exactInput', [params])) 323 | 324 | // encode the approves 325 | data.push(router.interface.encodeFunctionData('approveMax', [tokenA])) 326 | data.push(router.interface.encodeFunctionData('approveMax', [tokenB])) 327 | 328 | // encode the add liquidity 329 | const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA] 330 | const liquidityParams = { 331 | token0, 332 | token1, 333 | fee: FeeAmount.MEDIUM, 334 | tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 335 | tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]), 336 | recipient: trader.address, 337 | amount0Desired: amountOutMinimum, 338 | amount1Desired: amountOutMinimum, 339 | amount0Min: 0, 340 | amount1Min: 0, 341 | deadline: 2 ** 32, 342 | } 343 | data.push( 344 | router.interface.encodeFunctionData('callPositionManager', [ 345 | nft.interface.encodeFunctionData('mint', [liquidityParams]), 346 | ]) 347 | ) 348 | 349 | // encode the sweeps 350 | data.push(encodeSweepToken(tokenA, 0)) 351 | data.push(encodeSweepToken(tokenB, 0)) 352 | 353 | return router.connect(trader)['multicall(bytes[])'](data) 354 | } 355 | 356 | it('0 -> 1 and 0 -> 2', async () => { 357 | const amountIn = 1000 358 | const amountOutMinimum = 996 359 | 360 | // prep for the swap + add by sending tokens 361 | await tokens[0].transfer(trader.address, amountIn * 2) 362 | await tokens[0].connect(trader).approve(router.address, amountIn * 2) 363 | 364 | const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address) 365 | const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address) 366 | const traderToken2BalanceBefore = await tokens[2].balanceOf(trader.address) 367 | expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn * 2) 368 | expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0) 369 | expect(traderToken2BalanceBefore.toNumber()).to.be.eq(0) 370 | 371 | const traderNFTBalanceBefore = await nft.balanceOf(trader.address) 372 | expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0) 373 | 374 | await anyAssetAddExactInput(tokens[0].address, tokens[1].address, tokens[2].address, amountIn, amountOutMinimum) 375 | 376 | const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address) 377 | const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address) 378 | const traderToken2BalanceAfter = await tokens[2].balanceOf(trader.address) 379 | expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0) 380 | expect(traderToken1BalanceAfter.toNumber()).to.be.eq(0) 381 | expect(traderToken2BalanceAfter.toNumber()).to.be.eq(0) 382 | 383 | const traderNFTBalanceAfter = await nft.balanceOf(trader.address) 384 | expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1) 385 | }) 386 | }) 387 | }) 388 | }) 389 | --------------------------------------------------------------------------------