Added uniswap router contracts
This commit is contained in:
parent
98050890ed
commit
f086e9bac4
1
lib/swap-router-contracts/.gitattributes
vendored
Normal file
1
lib/swap-router-contracts/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sol linguist-language=Solidity
|
||||
33
lib/swap-router-contracts/.github/workflows/lint.yml
vendored
Normal file
33
lib/swap-router-contracts/.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@a8497ddb33fb1205941fd40452ca9fff07e0770d
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
prettier: true
|
||||
auto_fix: true
|
||||
prettier_extensions: 'css,html,js,json,jsx,md,sass,scss,ts,tsx,vue,yaml,yml,sol'
|
||||
45
lib/swap-router-contracts/.github/workflows/release.yml
vendored
Normal file
45
lib/swap-router-contracts/.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: Release
|
||||
|
||||
on: workflow_dispatch # TODO set this on a deploy schedule
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
environment:
|
||||
name: release
|
||||
runs-on:
|
||||
group: npm-deploy
|
||||
steps:
|
||||
- name: Load Secrets
|
||||
uses: 1password/load-secrets-action@581a835fb51b8e7ec56b71cf2ffddd7e68bb25e0
|
||||
with:
|
||||
# Export loaded secrets as environment variables
|
||||
export-env: true
|
||||
env:
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
NPM_TOKEN: op://npm-deploy/npm-runner-token/secret
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "true"
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
# Defaults to the user or organization that owns the workflow file
|
||||
scope: '@uniswap'
|
||||
|
||||
- name: Setup CI
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }}
|
||||
41
lib/swap-router-contracts/.github/workflows/tests.yml
vendored
Normal file
41
lib/swap-router-contracts/.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
# This is required separately from yarn test because it generates the typechain definitions
|
||||
- name: Compile
|
||||
run: yarn compile
|
||||
|
||||
- name: Run unit tests
|
||||
run: yarn test
|
||||
env: # Or as an environment variable
|
||||
ARCHIVE_RPC_URL: ${{ secrets.ARCHIVE_RPC_URL }}
|
||||
7
lib/swap-router-contracts/.gitignore
vendored
Normal file
7
lib/swap-router-contracts/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
artifacts/
|
||||
cache/
|
||||
crytic-export/
|
||||
node_modules/
|
||||
typechain/
|
||||
.env
|
||||
.vscode
|
||||
1
lib/swap-router-contracts/.prettierignore
Normal file
1
lib/swap-router-contracts/.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
.github
|
||||
5
lib/swap-router-contracts/.prettierrc
Normal file
5
lib/swap-router-contracts/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
6
lib/swap-router-contracts/.solhint.json
Normal file
6
lib/swap-router-contracts/.solhint.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
1
lib/swap-router-contracts/.yarnrc
Normal file
1
lib/swap-router-contracts/.yarnrc
Normal file
@ -0,0 +1 @@
|
||||
ignore-scripts true
|
||||
339
lib/swap-router-contracts/LICENSE
Normal file
339
lib/swap-router-contracts/LICENSE
Normal file
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
65
lib/swap-router-contracts/README.md
Normal file
65
lib/swap-router-contracts/README.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Uniswap Swap Router
|
||||
|
||||
[](https://github.com/Uniswap/swap-router-contracts/actions?query=workflow%3ATests)
|
||||
[](https://github.com/Uniswap/swap-router-contracts/actions?query=workflow%3ALint)
|
||||
|
||||
This repository contains smart contracts for swapping on the Uniswap V2 and V3 protocols.
|
||||
|
||||
## Bug bounty
|
||||
|
||||
This repository is subject to the Uniswap V3 bug bounty program,
|
||||
per the terms defined [here](./bug-bounty.md).
|
||||
|
||||
## Local deployment
|
||||
|
||||
In order to deploy this code to a local testnet, you should install the npm package
|
||||
`@uniswap/swap-router-contracts`
|
||||
and import bytecode imported from artifacts located at
|
||||
`@uniswap/swap-router-contracts/artifacts/contracts/*/*.json`.
|
||||
For example:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
abi as SWAP_ROUTER_ABI,
|
||||
bytecode as SWAP_ROUTER_BYTECODE,
|
||||
} from '@uniswap/swap-router-contracts/artifacts/contracts/SwapRouter02.sol/SwapRouter02.json'
|
||||
|
||||
// deploy the bytecode
|
||||
```
|
||||
|
||||
This will ensure that you are testing against the same bytecode that is deployed to
|
||||
mainnet and public testnets, and all Uniswap code will correctly interoperate with
|
||||
your local deployment.
|
||||
|
||||
## Using solidity interfaces
|
||||
|
||||
The swap router contract interfaces are available for import into solidity smart contracts
|
||||
via the npm artifact `@uniswap/swap-router-contracts`, e.g.:
|
||||
|
||||
```solidity
|
||||
import '@uniswap/swap-router-contracts/contracts/interfaces/ISwapRouter02.sol';
|
||||
|
||||
contract MyContract {
|
||||
ISwapRouter02 router;
|
||||
|
||||
function doSomethingWithSwapRouter() {
|
||||
// router.exactInput(...);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Some tests use Hardhat mainnet forking and therefore require an archive node.
|
||||
Either create a `.env` file in the workspace root containing:
|
||||
|
||||
```
|
||||
ARCHIVE_RPC_URL='...'
|
||||
```
|
||||
|
||||
Or set the variable when running tests:
|
||||
|
||||
```
|
||||
export ARCHIVE_RPC_URL='...' && npm run test
|
||||
```
|
||||
80
lib/swap-router-contracts/bug-bounty.md
Normal file
80
lib/swap-router-contracts/bug-bounty.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Uniswap V3 Bug Bounty
|
||||
|
||||
## Overview
|
||||
|
||||
Starting on September 16th, 2021, the [swap-router-contracts](https://github.com/Uniswap/swap-router-contracts) repository is
|
||||
subject to the Uniswap V3 Bug Bounty (the “Program”) to incentivize responsible bug disclosure.
|
||||
|
||||
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!
|
||||
|
||||
## Scope
|
||||
|
||||
The scope of the Program is limited to bugs that result in the loss of user funds.
|
||||
|
||||
The following are not within the scope of the Program:
|
||||
|
||||
- Any contract located under [contracts/test](./contracts/test) or [contracts/lens](./contracts/lens).
|
||||
- Bugs in any third party contract or platform.
|
||||
- Vulnerabilities already reported and/or discovered in contracts built by third parties.
|
||||
- Any already-reported bugs.
|
||||
|
||||
Vulnerabilities contingent upon the occurrence of any of the following also are outside the scope of this Program:
|
||||
|
||||
- Frontend bugs
|
||||
- DDOS attacks
|
||||
- Spamming
|
||||
- Phishing
|
||||
- Automated tools (Github Actions, AWS, etc.)
|
||||
- Compromise or misuse of third party systems or services
|
||||
|
||||
## Assumptions
|
||||
|
||||
Uniswap V3 was developed with the following assumptions, and thus any bug must also adhere to the following assumptions
|
||||
to be eligible for the bug bounty:
|
||||
|
||||
- The total supply of any token does not exceed 2<sup>128</sup> - 1, i.e. `type(uint128).max`.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Rewards
|
||||
|
||||
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.
|
||||
For critical bugs that lead to loss of user funds (more than 1% or user specified slippage tolerance),
|
||||
rewards of up to $500,000 will be granted. Lower severity bugs will be rewarded at the discretion of the team.
|
||||
In addition, all vulnerabilities disclosed prior to the mainnet launch date will be subject to receive higher rewards.
|
||||
|
||||
## Disclosure
|
||||
|
||||
Any vulnerability or bug discovered must be reported only to the following email: [security@uniswap.org](mailto:security@uniswap.org).
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- The conditions on which reproducing the bug is contingent.
|
||||
- The steps needed to reproduce the bug or, preferably, a proof of concept.
|
||||
- The potential implications of the vulnerability being abused.
|
||||
|
||||
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.
|
||||
|
||||
## Eligibility
|
||||
|
||||
To be eligible for a reward under this Program, you must:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- Provide sufficient information to enable our engineers to reproduce and fix the vulnerability.
|
||||
- Not engage in any unlawful conduct when disclosing the bug, including through threats, demands, or any other coercive tactics.
|
||||
- Not exploit the vulnerability in any way, including through making it public or by obtaining a profit (other than a reward under this Program).
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, interruption or degradation of Uniswap V2 or V3.
|
||||
- Submit only one vulnerability per submission, unless you need to chain vulnerabilities to provide impact regarding any of the vulnerabilities.
|
||||
- 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.
|
||||
- Not be one of our current or former employees, vendors, or contractors or an employee of any of those vendors or contractors.
|
||||
- Not be subject to US sanctions or reside in a US-embargoed country.
|
||||
- Be at least 18 years of age or, if younger, submit your vulnerability with the consent of your parent or guardian.
|
||||
|
||||
## Other Terms
|
||||
|
||||
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.
|
||||
|
||||
The terms and conditions of this Program may be altered at any time.
|
||||
22
lib/swap-router-contracts/contracts/SwapRouter02.sol
Normal file
22
lib/swap-router-contracts/contracts/SwapRouter02.sol
Normal file
@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/SelfPermit.sol';
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
|
||||
import './interfaces/ISwapRouter02.sol';
|
||||
import './V2SwapRouter.sol';
|
||||
import './V3SwapRouter.sol';
|
||||
import './base/ApproveAndCall.sol';
|
||||
import './base/MulticallExtended.sol';
|
||||
|
||||
/// @title Uniswap V2 and V3 Swap Router
|
||||
contract SwapRouter02 is ISwapRouter02, V2SwapRouter, V3SwapRouter, ApproveAndCall, MulticallExtended, SelfPermit {
|
||||
constructor(
|
||||
address _factoryV2,
|
||||
address factoryV3,
|
||||
address _positionManager,
|
||||
address _WETH9
|
||||
) ImmutableState(_factoryV2, _positionManager) PeripheryImmutableState(factoryV3, _WETH9) {}
|
||||
}
|
||||
94
lib/swap-router-contracts/contracts/V2SwapRouter.sol
Normal file
94
lib/swap-router-contracts/contracts/V2SwapRouter.sol
Normal file
@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol';
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
|
||||
import './interfaces/IV2SwapRouter.sol';
|
||||
import './base/ImmutableState.sol';
|
||||
import './base/PeripheryPaymentsWithFeeExtended.sol';
|
||||
import './libraries/Constants.sol';
|
||||
import './libraries/UniswapV2Library.sol';
|
||||
|
||||
/// @title Uniswap V2 Swap Router
|
||||
/// @notice Router for stateless execution of swaps against Uniswap V2
|
||||
abstract contract V2SwapRouter is IV2SwapRouter, ImmutableState, PeripheryPaymentsWithFeeExtended {
|
||||
using LowGasSafeMath for uint256;
|
||||
|
||||
// supports fee-on-transfer tokens
|
||||
// requires the initial amount to have already been sent to the first pair
|
||||
function _swap(address[] memory path, address _to) private {
|
||||
for (uint256 i; i < path.length - 1; i++) {
|
||||
(address input, address output) = (path[i], path[i + 1]);
|
||||
(address token0, ) = UniswapV2Library.sortTokens(input, output);
|
||||
IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factoryV2, input, output));
|
||||
uint256 amountInput;
|
||||
uint256 amountOutput;
|
||||
// scope to avoid stack too deep errors
|
||||
{
|
||||
(uint256 reserve0, uint256 reserve1, ) = pair.getReserves();
|
||||
(uint256 reserveInput, uint256 reserveOutput) =
|
||||
input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
|
||||
amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
|
||||
amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
|
||||
}
|
||||
(uint256 amount0Out, uint256 amount1Out) =
|
||||
input == token0 ? (uint256(0), amountOutput) : (amountOutput, uint256(0));
|
||||
address to = i < path.length - 2 ? UniswapV2Library.pairFor(factoryV2, output, path[i + 2]) : _to;
|
||||
pair.swap(amount0Out, amount1Out, to, new bytes(0));
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IV2SwapRouter
|
||||
function swapExactTokensForTokens(
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin,
|
||||
address[] calldata path,
|
||||
address to
|
||||
) external payable override returns (uint256 amountOut) {
|
||||
// use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract
|
||||
bool hasAlreadyPaid;
|
||||
if (amountIn == Constants.CONTRACT_BALANCE) {
|
||||
hasAlreadyPaid = true;
|
||||
amountIn = IERC20(path[0]).balanceOf(address(this));
|
||||
}
|
||||
|
||||
pay(
|
||||
path[0],
|
||||
hasAlreadyPaid ? address(this) : msg.sender,
|
||||
UniswapV2Library.pairFor(factoryV2, path[0], path[1]),
|
||||
amountIn
|
||||
);
|
||||
|
||||
// find and replace to addresses
|
||||
if (to == Constants.MSG_SENDER) to = msg.sender;
|
||||
else if (to == Constants.ADDRESS_THIS) to = address(this);
|
||||
|
||||
uint256 balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
|
||||
|
||||
_swap(path, to);
|
||||
|
||||
amountOut = IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore);
|
||||
require(amountOut >= amountOutMin, 'Too little received');
|
||||
}
|
||||
|
||||
/// @inheritdoc IV2SwapRouter
|
||||
function swapTokensForExactTokens(
|
||||
uint256 amountOut,
|
||||
uint256 amountInMax,
|
||||
address[] calldata path,
|
||||
address to
|
||||
) external payable override returns (uint256 amountIn) {
|
||||
amountIn = UniswapV2Library.getAmountsIn(factoryV2, amountOut, path)[0];
|
||||
require(amountIn <= amountInMax, 'Too much requested');
|
||||
|
||||
pay(path[0], msg.sender, UniswapV2Library.pairFor(factoryV2, path[0], path[1]), amountIn);
|
||||
|
||||
// find and replace to addresses
|
||||
if (to == Constants.MSG_SENDER) to = msg.sender;
|
||||
else if (to == Constants.ADDRESS_THIS) to = address(this);
|
||||
|
||||
_swap(path, to);
|
||||
}
|
||||
}
|
||||
238
lib/swap-router-contracts/contracts/V3SwapRouter.sol
Normal file
238
lib/swap-router-contracts/contracts/V3SwapRouter.sol
Normal file
@ -0,0 +1,238 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
|
||||
import './interfaces/IV3SwapRouter.sol';
|
||||
import './base/PeripheryPaymentsWithFeeExtended.sol';
|
||||
import './base/OracleSlippage.sol';
|
||||
import './libraries/Constants.sol';
|
||||
|
||||
/// @title Uniswap V3 Swap Router
|
||||
/// @notice Router for stateless execution of swaps against Uniswap V3
|
||||
abstract contract V3SwapRouter is IV3SwapRouter, PeripheryPaymentsWithFeeExtended, OracleSlippage {
|
||||
using Path for bytes;
|
||||
using SafeCast for uint256;
|
||||
|
||||
/// @dev Used as the placeholder value for amountInCached, because the computed amount in for an exact output swap
|
||||
/// can never actually be this value
|
||||
uint256 private constant DEFAULT_AMOUNT_IN_CACHED = type(uint256).max;
|
||||
|
||||
/// @dev Transient storage variable used for returning the computed amount in for an exact output swap.
|
||||
uint256 private amountInCached = DEFAULT_AMOUNT_IN_CACHED;
|
||||
|
||||
/// @dev Returns the pool for the given token pair and fee. The pool contract may or may not exist.
|
||||
function getPool(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) private view returns (IUniswapV3Pool) {
|
||||
return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
||||
}
|
||||
|
||||
struct SwapCallbackData {
|
||||
bytes path;
|
||||
address payer;
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3SwapCallback
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes calldata _data
|
||||
) external override {
|
||||
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
|
||||
SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData));
|
||||
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
|
||||
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
|
||||
|
||||
(bool isExactInput, uint256 amountToPay) =
|
||||
amount0Delta > 0
|
||||
? (tokenIn < tokenOut, uint256(amount0Delta))
|
||||
: (tokenOut < tokenIn, uint256(amount1Delta));
|
||||
|
||||
if (isExactInput) {
|
||||
pay(tokenIn, data.payer, msg.sender, amountToPay);
|
||||
} else {
|
||||
// either initiate the next swap or pay
|
||||
if (data.path.hasMultiplePools()) {
|
||||
data.path = data.path.skipToken();
|
||||
exactOutputInternal(amountToPay, msg.sender, 0, data);
|
||||
} else {
|
||||
amountInCached = amountToPay;
|
||||
// note that because exact output swaps are executed in reverse order, tokenOut is actually tokenIn
|
||||
pay(tokenOut, data.payer, msg.sender, amountToPay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Performs a single exact input swap
|
||||
function exactInputInternal(
|
||||
uint256 amountIn,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96,
|
||||
SwapCallbackData memory data
|
||||
) private returns (uint256 amountOut) {
|
||||
// find and replace recipient addresses
|
||||
if (recipient == Constants.MSG_SENDER) recipient = msg.sender;
|
||||
else if (recipient == Constants.ADDRESS_THIS) recipient = address(this);
|
||||
|
||||
(address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
|
||||
|
||||
bool zeroForOne = tokenIn < tokenOut;
|
||||
|
||||
(int256 amount0, int256 amount1) =
|
||||
getPool(tokenIn, tokenOut, fee).swap(
|
||||
recipient,
|
||||
zeroForOne,
|
||||
amountIn.toInt256(),
|
||||
sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: sqrtPriceLimitX96,
|
||||
abi.encode(data)
|
||||
);
|
||||
|
||||
return uint256(-(zeroForOne ? amount1 : amount0));
|
||||
}
|
||||
|
||||
/// @inheritdoc IV3SwapRouter
|
||||
function exactInputSingle(ExactInputSingleParams memory params)
|
||||
external
|
||||
payable
|
||||
override
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
// use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract
|
||||
bool hasAlreadyPaid;
|
||||
if (params.amountIn == Constants.CONTRACT_BALANCE) {
|
||||
hasAlreadyPaid = true;
|
||||
params.amountIn = IERC20(params.tokenIn).balanceOf(address(this));
|
||||
}
|
||||
|
||||
amountOut = exactInputInternal(
|
||||
params.amountIn,
|
||||
params.recipient,
|
||||
params.sqrtPriceLimitX96,
|
||||
SwapCallbackData({
|
||||
path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut),
|
||||
payer: hasAlreadyPaid ? address(this) : msg.sender
|
||||
})
|
||||
);
|
||||
require(amountOut >= params.amountOutMinimum, 'Too little received');
|
||||
}
|
||||
|
||||
/// @inheritdoc IV3SwapRouter
|
||||
function exactInput(ExactInputParams memory params) external payable override returns (uint256 amountOut) {
|
||||
// use amountIn == Constants.CONTRACT_BALANCE as a flag to swap the entire balance of the contract
|
||||
bool hasAlreadyPaid;
|
||||
if (params.amountIn == Constants.CONTRACT_BALANCE) {
|
||||
hasAlreadyPaid = true;
|
||||
(address tokenIn, , ) = params.path.decodeFirstPool();
|
||||
params.amountIn = IERC20(tokenIn).balanceOf(address(this));
|
||||
}
|
||||
|
||||
address payer = hasAlreadyPaid ? address(this) : msg.sender;
|
||||
|
||||
while (true) {
|
||||
bool hasMultiplePools = params.path.hasMultiplePools();
|
||||
|
||||
// the outputs of prior swaps become the inputs to subsequent ones
|
||||
params.amountIn = exactInputInternal(
|
||||
params.amountIn,
|
||||
hasMultiplePools ? address(this) : params.recipient, // for intermediate swaps, this contract custodies
|
||||
0,
|
||||
SwapCallbackData({
|
||||
path: params.path.getFirstPool(), // only the first pool in the path is necessary
|
||||
payer: payer
|
||||
})
|
||||
);
|
||||
|
||||
// decide whether to continue or terminate
|
||||
if (hasMultiplePools) {
|
||||
payer = address(this);
|
||||
params.path = params.path.skipToken();
|
||||
} else {
|
||||
amountOut = params.amountIn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
require(amountOut >= params.amountOutMinimum, 'Too little received');
|
||||
}
|
||||
|
||||
/// @dev Performs a single exact output swap
|
||||
function exactOutputInternal(
|
||||
uint256 amountOut,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96,
|
||||
SwapCallbackData memory data
|
||||
) private returns (uint256 amountIn) {
|
||||
// find and replace recipient addresses
|
||||
if (recipient == Constants.MSG_SENDER) recipient = msg.sender;
|
||||
else if (recipient == Constants.ADDRESS_THIS) recipient = address(this);
|
||||
|
||||
(address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool();
|
||||
|
||||
bool zeroForOne = tokenIn < tokenOut;
|
||||
|
||||
(int256 amount0Delta, int256 amount1Delta) =
|
||||
getPool(tokenIn, tokenOut, fee).swap(
|
||||
recipient,
|
||||
zeroForOne,
|
||||
-amountOut.toInt256(),
|
||||
sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: sqrtPriceLimitX96,
|
||||
abi.encode(data)
|
||||
);
|
||||
|
||||
uint256 amountOutReceived;
|
||||
(amountIn, amountOutReceived) = zeroForOne
|
||||
? (uint256(amount0Delta), uint256(-amount1Delta))
|
||||
: (uint256(amount1Delta), uint256(-amount0Delta));
|
||||
// it's technically possible to not receive the full output amount,
|
||||
// so if no price limit has been specified, require this possibility away
|
||||
if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);
|
||||
}
|
||||
|
||||
/// @inheritdoc IV3SwapRouter
|
||||
function exactOutputSingle(ExactOutputSingleParams calldata params)
|
||||
external
|
||||
payable
|
||||
override
|
||||
returns (uint256 amountIn)
|
||||
{
|
||||
// avoid an SLOAD by using the swap return data
|
||||
amountIn = exactOutputInternal(
|
||||
params.amountOut,
|
||||
params.recipient,
|
||||
params.sqrtPriceLimitX96,
|
||||
SwapCallbackData({path: abi.encodePacked(params.tokenOut, params.fee, params.tokenIn), payer: msg.sender})
|
||||
);
|
||||
|
||||
require(amountIn <= params.amountInMaximum, 'Too much requested');
|
||||
// has to be reset even though we don't use it in the single hop case
|
||||
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
|
||||
}
|
||||
|
||||
/// @inheritdoc IV3SwapRouter
|
||||
function exactOutput(ExactOutputParams calldata params) external payable override returns (uint256 amountIn) {
|
||||
exactOutputInternal(
|
||||
params.amountOut,
|
||||
params.recipient,
|
||||
0,
|
||||
SwapCallbackData({path: params.path, payer: msg.sender})
|
||||
);
|
||||
|
||||
amountIn = amountInCached;
|
||||
require(amountIn <= params.amountInMaximum, 'Too much requested');
|
||||
amountInCached = DEFAULT_AMOUNT_IN_CACHED;
|
||||
}
|
||||
}
|
||||
126
lib/swap-router-contracts/contracts/base/ApproveAndCall.sol
Normal file
126
lib/swap-router-contracts/contracts/base/ApproveAndCall.sol
Normal file
@ -0,0 +1,126 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import '@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol';
|
||||
|
||||
import '../interfaces/IApproveAndCall.sol';
|
||||
import './ImmutableState.sol';
|
||||
|
||||
/// @title Approve and Call
|
||||
/// @notice Allows callers to approve the Uniswap V3 position manager from this contract,
|
||||
/// for any token, and then make calls into the position manager
|
||||
abstract contract ApproveAndCall is IApproveAndCall, ImmutableState {
|
||||
function tryApprove(address token, uint256 amount) private returns (bool) {
|
||||
(bool success, bytes memory data) =
|
||||
token.call(abi.encodeWithSelector(IERC20.approve.selector, positionManager, amount));
|
||||
return success && (data.length == 0 || abi.decode(data, (bool)));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function getApprovalType(address token, uint256 amount) external override returns (ApprovalType) {
|
||||
// check existing approval
|
||||
if (IERC20(token).allowance(address(this), positionManager) >= amount) return ApprovalType.NOT_REQUIRED;
|
||||
|
||||
// try type(uint256).max / type(uint256).max - 1
|
||||
if (tryApprove(token, type(uint256).max)) return ApprovalType.MAX;
|
||||
if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.MAX_MINUS_ONE;
|
||||
|
||||
// set approval to 0 (must succeed)
|
||||
require(tryApprove(token, 0));
|
||||
|
||||
// try type(uint256).max / type(uint256).max - 1
|
||||
if (tryApprove(token, type(uint256).max)) return ApprovalType.ZERO_THEN_MAX;
|
||||
if (tryApprove(token, type(uint256).max - 1)) return ApprovalType.ZERO_THEN_MAX_MINUS_ONE;
|
||||
|
||||
revert();
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function approveMax(address token) external payable override {
|
||||
require(tryApprove(token, type(uint256).max));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function approveMaxMinusOne(address token) external payable override {
|
||||
require(tryApprove(token, type(uint256).max - 1));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function approveZeroThenMax(address token) external payable override {
|
||||
require(tryApprove(token, 0));
|
||||
require(tryApprove(token, type(uint256).max));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function approveZeroThenMaxMinusOne(address token) external payable override {
|
||||
require(tryApprove(token, 0));
|
||||
require(tryApprove(token, type(uint256).max - 1));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function callPositionManager(bytes memory data) public payable override returns (bytes memory result) {
|
||||
bool success;
|
||||
(success, result) = positionManager.call(data);
|
||||
|
||||
if (!success) {
|
||||
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
|
||||
if (result.length < 68) revert();
|
||||
assembly {
|
||||
result := add(result, 0x04)
|
||||
}
|
||||
revert(abi.decode(result, (string)));
|
||||
}
|
||||
}
|
||||
|
||||
function balanceOf(address token) private view returns (uint256) {
|
||||
return IERC20(token).balanceOf(address(this));
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function mint(MintParams calldata params) external payable override returns (bytes memory result) {
|
||||
return
|
||||
callPositionManager(
|
||||
abi.encodeWithSelector(
|
||||
INonfungiblePositionManager.mint.selector,
|
||||
INonfungiblePositionManager.MintParams({
|
||||
token0: params.token0,
|
||||
token1: params.token1,
|
||||
fee: params.fee,
|
||||
tickLower: params.tickLower,
|
||||
tickUpper: params.tickUpper,
|
||||
amount0Desired: balanceOf(params.token0),
|
||||
amount1Desired: balanceOf(params.token1),
|
||||
amount0Min: params.amount0Min,
|
||||
amount1Min: params.amount1Min,
|
||||
recipient: params.recipient,
|
||||
deadline: type(uint256).max // deadline should be checked via multicall
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// @inheritdoc IApproveAndCall
|
||||
function increaseLiquidity(IncreaseLiquidityParams calldata params)
|
||||
external
|
||||
payable
|
||||
override
|
||||
returns (bytes memory result)
|
||||
{
|
||||
return
|
||||
callPositionManager(
|
||||
abi.encodeWithSelector(
|
||||
INonfungiblePositionManager.increaseLiquidity.selector,
|
||||
INonfungiblePositionManager.IncreaseLiquidityParams({
|
||||
tokenId: params.tokenId,
|
||||
amount0Desired: balanceOf(params.token0),
|
||||
amount1Desired: balanceOf(params.token1),
|
||||
amount0Min: params.amount0Min,
|
||||
amount1Min: params.amount1Min,
|
||||
deadline: type(uint256).max // deadline should be checked via multicall
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/swap-router-contracts/contracts/base/ImmutableState.sol
Normal file
18
lib/swap-router-contracts/contracts/base/ImmutableState.sol
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '../interfaces/IImmutableState.sol';
|
||||
|
||||
/// @title Immutable state
|
||||
/// @notice Immutable state used by the swap router
|
||||
abstract contract ImmutableState is IImmutableState {
|
||||
/// @inheritdoc IImmutableState
|
||||
address public immutable override factoryV2;
|
||||
/// @inheritdoc IImmutableState
|
||||
address public immutable override positionManager;
|
||||
|
||||
constructor(address _factoryV2, address _positionManager) {
|
||||
factoryV2 = _factoryV2;
|
||||
positionManager = _positionManager;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/Multicall.sol';
|
||||
|
||||
import '../interfaces/IMulticallExtended.sol';
|
||||
import '../base/PeripheryValidationExtended.sol';
|
||||
|
||||
/// @title Multicall
|
||||
/// @notice Enables calling multiple methods in a single call to the contract
|
||||
abstract contract MulticallExtended is IMulticallExtended, Multicall, PeripheryValidationExtended {
|
||||
/// @inheritdoc IMulticallExtended
|
||||
function multicall(uint256 deadline, bytes[] calldata data)
|
||||
external
|
||||
payable
|
||||
override
|
||||
checkDeadline(deadline)
|
||||
returns (bytes[] memory)
|
||||
{
|
||||
return multicall(data);
|
||||
}
|
||||
|
||||
/// @inheritdoc IMulticallExtended
|
||||
function multicall(bytes32 previousBlockhash, bytes[] calldata data)
|
||||
external
|
||||
payable
|
||||
override
|
||||
checkPreviousBlockhash(previousBlockhash)
|
||||
returns (bytes[] memory)
|
||||
{
|
||||
return multicall(data);
|
||||
}
|
||||
}
|
||||
171
lib/swap-router-contracts/contracts/base/OracleSlippage.sol
Normal file
171
lib/swap-router-contracts/contracts/base/OracleSlippage.sol
Normal file
@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '../interfaces/IOracleSlippage.sol';
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
import '@uniswap/v3-periphery/contracts/base/BlockTimestamp.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol';
|
||||
|
||||
abstract contract OracleSlippage is IOracleSlippage, PeripheryImmutableState, BlockTimestamp {
|
||||
using Path for bytes;
|
||||
|
||||
/// @dev Returns the tick as of the beginning of the current block, and as of right now, for the given pool.
|
||||
function getBlockStartingAndCurrentTick(IUniswapV3Pool pool)
|
||||
internal
|
||||
view
|
||||
returns (int24 blockStartingTick, int24 currentTick)
|
||||
{
|
||||
uint16 observationIndex;
|
||||
uint16 observationCardinality;
|
||||
(, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0();
|
||||
|
||||
// 2 observations are needed to reliably calculate the block starting tick
|
||||
require(observationCardinality > 1, 'NEO');
|
||||
|
||||
// If the latest observation occurred in the past, then no tick-changing trades have happened in this block
|
||||
// therefore the tick in `slot0` is the same as at the beginning of the current block.
|
||||
// We don't need to check if this observation is initialized - it is guaranteed to be.
|
||||
(uint32 observationTimestamp, int56 tickCumulative, , ) = pool.observations(observationIndex);
|
||||
if (observationTimestamp != uint32(_blockTimestamp())) {
|
||||
blockStartingTick = currentTick;
|
||||
} else {
|
||||
uint256 prevIndex = (uint256(observationIndex) + observationCardinality - 1) % observationCardinality;
|
||||
(uint32 prevObservationTimestamp, int56 prevTickCumulative, , bool prevInitialized) =
|
||||
pool.observations(prevIndex);
|
||||
|
||||
require(prevInitialized, 'ONI');
|
||||
|
||||
uint32 delta = observationTimestamp - prevObservationTimestamp;
|
||||
blockStartingTick = int24((tickCumulative - prevTickCumulative) / delta);
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Virtual function to get pool addresses that can be overridden in tests.
|
||||
function getPoolAddress(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) internal view virtual returns (IUniswapV3Pool pool) {
|
||||
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
||||
}
|
||||
|
||||
/// @dev Returns the synthetic time-weighted average tick as of secondsAgo, as well as the current tick,
|
||||
/// for the given path. Returned synthetic ticks always represent tokenOut/tokenIn prices,
|
||||
/// meaning lower ticks are worse.
|
||||
function getSyntheticTicks(bytes memory path, uint32 secondsAgo)
|
||||
internal
|
||||
view
|
||||
returns (int256 syntheticAverageTick, int256 syntheticCurrentTick)
|
||||
{
|
||||
bool lowerTicksAreWorse;
|
||||
|
||||
uint256 numPools = path.numPools();
|
||||
address previousTokenIn;
|
||||
for (uint256 i = 0; i < numPools; i++) {
|
||||
// this assumes the path is sorted in swap order
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
IUniswapV3Pool pool = getPoolAddress(tokenIn, tokenOut, fee);
|
||||
|
||||
// get the average and current ticks for the current pool
|
||||
int256 averageTick;
|
||||
int256 currentTick;
|
||||
if (secondsAgo == 0) {
|
||||
// we optimize for the secondsAgo == 0 case, i.e. since the beginning of the block
|
||||
(averageTick, currentTick) = getBlockStartingAndCurrentTick(pool);
|
||||
} else {
|
||||
(averageTick, ) = OracleLibrary.consult(address(pool), secondsAgo);
|
||||
(, currentTick, , , , , ) = IUniswapV3Pool(pool).slot0();
|
||||
}
|
||||
|
||||
if (i == numPools - 1) {
|
||||
// if we're here, this is the last pool in the path, meaning tokenOut represents the
|
||||
// destination token. so, if tokenIn < tokenOut, then tokenIn is token0 of the last pool,
|
||||
// meaning the current running ticks are going to represent tokenOut/tokenIn prices.
|
||||
// so, the lower these prices get, the worse of a price the swap will get
|
||||
lowerTicksAreWorse = tokenIn < tokenOut;
|
||||
} else {
|
||||
// if we're here, we need to iterate over the next pool in the path
|
||||
path = path.skipToken();
|
||||
previousTokenIn = tokenIn;
|
||||
}
|
||||
|
||||
// accumulate the ticks derived from the current pool into the running synthetic ticks,
|
||||
// ensuring that intermediate tokens "cancel out"
|
||||
bool add = (i == 0) || (previousTokenIn < tokenIn ? tokenIn < tokenOut : tokenOut < tokenIn);
|
||||
if (add) {
|
||||
syntheticAverageTick += averageTick;
|
||||
syntheticCurrentTick += currentTick;
|
||||
} else {
|
||||
syntheticAverageTick -= averageTick;
|
||||
syntheticCurrentTick -= currentTick;
|
||||
}
|
||||
}
|
||||
|
||||
// flip the sign of the ticks if necessary, to ensure that the lower ticks are always worse
|
||||
if (!lowerTicksAreWorse) {
|
||||
syntheticAverageTick *= -1;
|
||||
syntheticCurrentTick *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Cast a int256 to a int24, revert on overflow or underflow
|
||||
function toInt24(int256 y) private pure returns (int24 z) {
|
||||
require((z = int24(y)) == y);
|
||||
}
|
||||
|
||||
/// @dev For each passed path, fetches the synthetic time-weighted average tick as of secondsAgo,
|
||||
/// as well as the current tick. Then, synthetic ticks from all paths are subjected to a weighted
|
||||
/// average, where the weights are the fraction of the total input amount allocated to each path.
|
||||
/// Returned synthetic ticks always represent tokenOut/tokenIn prices, meaning lower ticks are worse.
|
||||
/// Paths must all start and end in the same token.
|
||||
function getSyntheticTicks(
|
||||
bytes[] memory paths,
|
||||
uint128[] memory amounts,
|
||||
uint32 secondsAgo
|
||||
) internal view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) {
|
||||
require(paths.length == amounts.length);
|
||||
|
||||
OracleLibrary.WeightedTickData[] memory weightedSyntheticAverageTicks =
|
||||
new OracleLibrary.WeightedTickData[](paths.length);
|
||||
OracleLibrary.WeightedTickData[] memory weightedSyntheticCurrentTicks =
|
||||
new OracleLibrary.WeightedTickData[](paths.length);
|
||||
|
||||
for (uint256 i = 0; i < paths.length; i++) {
|
||||
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(paths[i], secondsAgo);
|
||||
weightedSyntheticAverageTicks[i].tick = toInt24(syntheticAverageTick);
|
||||
weightedSyntheticCurrentTicks[i].tick = toInt24(syntheticCurrentTick);
|
||||
weightedSyntheticAverageTicks[i].weight = amounts[i];
|
||||
weightedSyntheticCurrentTicks[i].weight = amounts[i];
|
||||
}
|
||||
|
||||
averageSyntheticAverageTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticAverageTicks);
|
||||
averageSyntheticCurrentTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticCurrentTicks);
|
||||
}
|
||||
|
||||
/// @inheritdoc IOracleSlippage
|
||||
function checkOracleSlippage(
|
||||
bytes memory path,
|
||||
uint24 maximumTickDivergence,
|
||||
uint32 secondsAgo
|
||||
) external view override {
|
||||
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(path, secondsAgo);
|
||||
require(syntheticAverageTick - syntheticCurrentTick < maximumTickDivergence, 'TD');
|
||||
}
|
||||
|
||||
/// @inheritdoc IOracleSlippage
|
||||
function checkOracleSlippage(
|
||||
bytes[] memory paths,
|
||||
uint128[] memory amounts,
|
||||
uint24 maximumTickDivergence,
|
||||
uint32 secondsAgo
|
||||
) external view override {
|
||||
(int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) =
|
||||
getSyntheticTicks(paths, amounts, secondsAgo);
|
||||
require(averageSyntheticAverageTick - averageSyntheticCurrentTick < maximumTickDivergence, 'TD');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryPayments.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';
|
||||
|
||||
import '../interfaces/IPeripheryPaymentsExtended.sol';
|
||||
|
||||
abstract contract PeripheryPaymentsExtended is IPeripheryPaymentsExtended, PeripheryPayments {
|
||||
/// @inheritdoc IPeripheryPaymentsExtended
|
||||
function unwrapWETH9(uint256 amountMinimum) external payable override {
|
||||
unwrapWETH9(amountMinimum, msg.sender);
|
||||
}
|
||||
|
||||
/// @inheritdoc IPeripheryPaymentsExtended
|
||||
function wrapETH(uint256 value) external payable override {
|
||||
IWETH9(WETH9).deposit{value: value}();
|
||||
}
|
||||
|
||||
/// @inheritdoc IPeripheryPaymentsExtended
|
||||
function sweepToken(address token, uint256 amountMinimum) external payable override {
|
||||
sweepToken(token, amountMinimum, msg.sender);
|
||||
}
|
||||
|
||||
/// @inheritdoc IPeripheryPaymentsExtended
|
||||
function pull(address token, uint256 value) external payable override {
|
||||
TransferHelper.safeTransferFrom(token, msg.sender, address(this), value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryPaymentsWithFee.sol';
|
||||
|
||||
import '../interfaces/IPeripheryPaymentsWithFeeExtended.sol';
|
||||
import './PeripheryPaymentsExtended.sol';
|
||||
|
||||
abstract contract PeripheryPaymentsWithFeeExtended is
|
||||
IPeripheryPaymentsWithFeeExtended,
|
||||
PeripheryPaymentsExtended,
|
||||
PeripheryPaymentsWithFee
|
||||
{
|
||||
/// @inheritdoc IPeripheryPaymentsWithFeeExtended
|
||||
function unwrapWETH9WithFee(
|
||||
uint256 amountMinimum,
|
||||
uint256 feeBips,
|
||||
address feeRecipient
|
||||
) external payable override {
|
||||
unwrapWETH9WithFee(amountMinimum, msg.sender, feeBips, feeRecipient);
|
||||
}
|
||||
|
||||
/// @inheritdoc IPeripheryPaymentsWithFeeExtended
|
||||
function sweepTokenWithFee(
|
||||
address token,
|
||||
uint256 amountMinimum,
|
||||
uint256 feeBips,
|
||||
address feeRecipient
|
||||
) external payable override {
|
||||
sweepTokenWithFee(token, amountMinimum, msg.sender, feeBips, feeRecipient);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryValidation.sol';
|
||||
|
||||
abstract contract PeripheryValidationExtended is PeripheryValidation {
|
||||
modifier checkPreviousBlockhash(bytes32 previousBlockhash) {
|
||||
require(blockhash(block.number - 1) == previousBlockhash, 'Blockhash');
|
||||
_;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
interface IApproveAndCall {
|
||||
enum ApprovalType {NOT_REQUIRED, MAX, MAX_MINUS_ONE, ZERO_THEN_MAX, ZERO_THEN_MAX_MINUS_ONE}
|
||||
|
||||
/// @dev Lens to be called off-chain to determine which (if any) of the relevant approval functions should be called
|
||||
/// @param token The token to approve
|
||||
/// @param amount The amount to approve
|
||||
/// @return The required approval type
|
||||
function getApprovalType(address token, uint256 amount) external returns (ApprovalType);
|
||||
|
||||
/// @notice Approves a token for the maximum possible amount
|
||||
/// @param token The token to approve
|
||||
function approveMax(address token) external payable;
|
||||
|
||||
/// @notice Approves a token for the maximum possible amount minus one
|
||||
/// @param token The token to approve
|
||||
function approveMaxMinusOne(address token) external payable;
|
||||
|
||||
/// @notice Approves a token for zero, then the maximum possible amount
|
||||
/// @param token The token to approve
|
||||
function approveZeroThenMax(address token) external payable;
|
||||
|
||||
/// @notice Approves a token for zero, then the maximum possible amount minus one
|
||||
/// @param token The token to approve
|
||||
function approveZeroThenMaxMinusOne(address token) external payable;
|
||||
|
||||
/// @notice Calls the position manager with arbitrary calldata
|
||||
/// @param data Calldata to pass along to the position manager
|
||||
/// @return result The result from the call
|
||||
function callPositionManager(bytes memory data) external payable returns (bytes memory result);
|
||||
|
||||
struct MintParams {
|
||||
address token0;
|
||||
address token1;
|
||||
uint24 fee;
|
||||
int24 tickLower;
|
||||
int24 tickUpper;
|
||||
uint256 amount0Min;
|
||||
uint256 amount1Min;
|
||||
address recipient;
|
||||
}
|
||||
|
||||
/// @notice Calls the position manager's mint function
|
||||
/// @param params Calldata to pass along to the position manager
|
||||
/// @return result The result from the call
|
||||
function mint(MintParams calldata params) external payable returns (bytes memory result);
|
||||
|
||||
struct IncreaseLiquidityParams {
|
||||
address token0;
|
||||
address token1;
|
||||
uint256 tokenId;
|
||||
uint256 amount0Min;
|
||||
uint256 amount1Min;
|
||||
}
|
||||
|
||||
/// @notice Calls the position manager's increaseLiquidity function
|
||||
/// @param params Calldata to pass along to the position manager
|
||||
/// @return result The result from the call
|
||||
function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (bytes memory result);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.5.0;
|
||||
|
||||
/// @title Immutable state
|
||||
/// @notice Functions that return immutable state of the router
|
||||
interface IImmutableState {
|
||||
/// @return Returns the address of the Uniswap V2 factory
|
||||
function factoryV2() external view returns (address);
|
||||
|
||||
/// @return Returns the address of Uniswap V3 NFT position manager
|
||||
function positionManager() external view returns (address);
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @title MixedRouteQuoterV1 Interface
|
||||
/// @notice Supports quoting the calculated amounts for exact input swaps. Is specialized for routes containing a mix of V2 and V3 liquidity.
|
||||
/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap.
|
||||
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting
|
||||
/// to compute the result. They are also not gas efficient and should not be called on-chain.
|
||||
interface IMixedRouteQuoterV1 {
|
||||
/// @notice Returns the amount out received for a given exact input swap without executing the swap
|
||||
/// @param path The path of the swap, i.e. each token pair and the pool fee
|
||||
/// @param amountIn The amount of the first token to swap
|
||||
/// @return amountOut The amount of the last token that would be received
|
||||
/// @return v3SqrtPriceX96AfterList List of the sqrt price after the swap for each v3 pool in the path, 0 for v2 pools
|
||||
/// @return v3InitializedTicksCrossedList List of the initialized ticks that the swap crossed for each v3 pool in the path, 0 for v2 pools
|
||||
/// @return v3SwapGasEstimate The estimate of the gas that the v3 swaps in the path consume
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn)
|
||||
external
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160[] memory v3SqrtPriceX96AfterList,
|
||||
uint32[] memory v3InitializedTicksCrossedList,
|
||||
uint256 v3SwapGasEstimate
|
||||
);
|
||||
|
||||
struct QuoteExactInputSingleV3Params {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint256 amountIn;
|
||||
uint24 fee;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
struct QuoteExactInputSingleV2Params {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint256 amountIn;
|
||||
}
|
||||
|
||||
/// @notice Returns the amount out received for a given exact input but for a swap of a single pool
|
||||
/// @param params The params for the quote, encoded as `QuoteExactInputSingleParams`
|
||||
/// tokenIn The token being swapped in
|
||||
/// tokenOut The token being swapped out
|
||||
/// fee The fee of the token pool to consider for the pair
|
||||
/// amountIn The desired input amount
|
||||
/// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap
|
||||
/// @return amountOut The amount of `tokenOut` that would be received
|
||||
/// @return sqrtPriceX96After The sqrt price of the pool after the swap
|
||||
/// @return initializedTicksCrossed The number of initialized ticks that the swap crossed
|
||||
/// @return gasEstimate The estimate of the gas that the swap consumes
|
||||
function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params)
|
||||
external
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
);
|
||||
|
||||
/// @notice Returns the amount out received for a given exact input but for a swap of a single V2 pool
|
||||
/// @param params The params for the quote, encoded as `QuoteExactInputSingleV2Params`
|
||||
/// tokenIn The token being swapped in
|
||||
/// tokenOut The token being swapped out
|
||||
/// amountIn The desired input amount
|
||||
/// @return amountOut The amount of `tokenOut` that would be received
|
||||
function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params) external returns (uint256 amountOut);
|
||||
|
||||
/// @dev ExactOutput swaps are not supported by this new Quoter which is specialized for supporting routes
|
||||
/// crossing both V2 liquidity pairs and V3 pools.
|
||||
/// @deprecated quoteExactOutputSingle and exactOutput. Use QuoterV2 instead.
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/interfaces/IMulticall.sol';
|
||||
|
||||
/// @title MulticallExtended interface
|
||||
/// @notice Enables calling multiple methods in a single call to the contract with optional validation
|
||||
interface IMulticallExtended is IMulticall {
|
||||
/// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
|
||||
/// @dev The `msg.value` should not be trusted for any method callable from multicall.
|
||||
/// @param deadline The time by which this function must be called before failing
|
||||
/// @param data The encoded function data for each of the calls to make to this contract
|
||||
/// @return results The results from each of the calls passed in via data
|
||||
function multicall(uint256 deadline, bytes[] calldata data) external payable returns (bytes[] memory results);
|
||||
|
||||
/// @notice Call multiple functions in the current contract and return the data from all of them if they all succeed
|
||||
/// @dev The `msg.value` should not be trusted for any method callable from multicall.
|
||||
/// @param previousBlockhash The expected parent blockHash
|
||||
/// @param data The encoded function data for each of the calls to make to this contract
|
||||
/// @return results The results from each of the calls passed in via data
|
||||
function multicall(bytes32 previousBlockhash, bytes[] calldata data)
|
||||
external
|
||||
payable
|
||||
returns (bytes[] memory results);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @title OracleSlippage interface
|
||||
/// @notice Enables slippage checks against oracle prices
|
||||
interface IOracleSlippage {
|
||||
/// @notice Ensures that the current (synthetic) tick over the path is no worse than
|
||||
/// `maximumTickDivergence` ticks away from the average as of `secondsAgo`
|
||||
/// @param path The path to fetch prices over
|
||||
/// @param maximumTickDivergence The maximum number of ticks that the price can degrade by
|
||||
/// @param secondsAgo The number of seconds ago to compute oracle prices against
|
||||
function checkOracleSlippage(
|
||||
bytes memory path,
|
||||
uint24 maximumTickDivergence,
|
||||
uint32 secondsAgo
|
||||
) external view;
|
||||
|
||||
/// @notice Ensures that the weighted average current (synthetic) tick over the path is no
|
||||
/// worse than `maximumTickDivergence` ticks away from the average as of `secondsAgo`
|
||||
/// @param paths The paths to fetch prices over
|
||||
/// @param amounts The weights for each entry in `paths`
|
||||
/// @param maximumTickDivergence The maximum number of ticks that the price can degrade by
|
||||
/// @param secondsAgo The number of seconds ago to compute oracle prices against
|
||||
function checkOracleSlippage(
|
||||
bytes[] memory paths,
|
||||
uint128[] memory amounts,
|
||||
uint24 maximumTickDivergence,
|
||||
uint32 secondsAgo
|
||||
) external view;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol';
|
||||
|
||||
/// @title Periphery Payments Extended
|
||||
/// @notice Functions to ease deposits and withdrawals of ETH and tokens
|
||||
interface IPeripheryPaymentsExtended is IPeripheryPayments {
|
||||
/// @notice Unwraps the contract's WETH9 balance and sends it to msg.sender as ETH.
|
||||
/// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users.
|
||||
/// @param amountMinimum The minimum amount of WETH9 to unwrap
|
||||
function unwrapWETH9(uint256 amountMinimum) external payable;
|
||||
|
||||
/// @notice Wraps the contract's ETH balance into WETH9
|
||||
/// @dev The resulting WETH9 is custodied by the router, thus will require further distribution
|
||||
/// @param value The amount of ETH to wrap
|
||||
function wrapETH(uint256 value) external payable;
|
||||
|
||||
/// @notice Transfers the full amount of a token held by this contract to msg.sender
|
||||
/// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users
|
||||
/// @param token The contract address of the token which will be transferred to msg.sender
|
||||
/// @param amountMinimum The minimum amount of token required for a transfer
|
||||
function sweepToken(address token, uint256 amountMinimum) external payable;
|
||||
|
||||
/// @notice Transfers the specified amount of a token from the msg.sender to address(this)
|
||||
/// @param token The token to pull
|
||||
/// @param value The amount to pay
|
||||
function pull(address token, uint256 value) external payable;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/interfaces/IPeripheryPaymentsWithFee.sol';
|
||||
|
||||
import './IPeripheryPaymentsExtended.sol';
|
||||
|
||||
/// @title Periphery Payments With Fee Extended
|
||||
/// @notice Functions to ease deposits and withdrawals of ETH
|
||||
interface IPeripheryPaymentsWithFeeExtended is IPeripheryPaymentsExtended, IPeripheryPaymentsWithFee {
|
||||
/// @notice Unwraps the contract's WETH9 balance and sends it to msg.sender as ETH, with a percentage between
|
||||
/// 0 (exclusive), and 1 (inclusive) going to feeRecipient
|
||||
/// @dev The amountMinimum parameter prevents malicious contracts from stealing WETH9 from users.
|
||||
function unwrapWETH9WithFee(
|
||||
uint256 amountMinimum,
|
||||
uint256 feeBips,
|
||||
address feeRecipient
|
||||
) external payable;
|
||||
|
||||
/// @notice Transfers the full amount of a token held by this contract to msg.sender, with a percentage between
|
||||
/// 0 (exclusive) and 1 (inclusive) going to feeRecipient
|
||||
/// @dev The amountMinimum parameter prevents malicious contracts from stealing the token from users
|
||||
function sweepTokenWithFee(
|
||||
address token,
|
||||
uint256 amountMinimum,
|
||||
uint256 feeBips,
|
||||
address feeRecipient
|
||||
) external payable;
|
||||
}
|
||||
51
lib/swap-router-contracts/contracts/interfaces/IQuoter.sol
Normal file
51
lib/swap-router-contracts/contracts/interfaces/IQuoter.sol
Normal file
@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @title Quoter Interface
|
||||
/// @notice Supports quoting the calculated amounts from exact input or exact output swaps
|
||||
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting
|
||||
/// to compute the result. They are also not gas efficient and should not be called on-chain.
|
||||
interface IQuoter {
|
||||
/// @notice Returns the amount out received for a given exact input swap without executing the swap
|
||||
/// @param path The path of the swap, i.e. each token pair and the pool fee
|
||||
/// @param amountIn The amount of the first token to swap
|
||||
/// @return amountOut The amount of the last token that would be received
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut);
|
||||
|
||||
/// @notice Returns the amount out received for a given exact input but for a swap of a single pool
|
||||
/// @param tokenIn The token being swapped in
|
||||
/// @param tokenOut The token being swapped out
|
||||
/// @param fee The fee of the token pool to consider for the pair
|
||||
/// @param amountIn The desired input amount
|
||||
/// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap
|
||||
/// @return amountOut The amount of `tokenOut` that would be received
|
||||
function quoteExactInputSingle(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountIn,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external returns (uint256 amountOut);
|
||||
|
||||
/// @notice Returns the amount in required for a given exact output swap without executing the swap
|
||||
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
|
||||
/// @param amountOut The amount of the last token to receive
|
||||
/// @return amountIn The amount of first token required to be paid
|
||||
function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn);
|
||||
|
||||
/// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool
|
||||
/// @param tokenIn The token being swapped in
|
||||
/// @param tokenOut The token being swapped out
|
||||
/// @param fee The fee of the token pool to consider for the pair
|
||||
/// @param amountOut The desired output amount
|
||||
/// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap
|
||||
/// @return amountIn The amount required as the input for the swap in order to receive `amountOut`
|
||||
function quoteExactOutputSingle(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external returns (uint256 amountIn);
|
||||
}
|
||||
98
lib/swap-router-contracts/contracts/interfaces/IQuoterV2.sol
Normal file
98
lib/swap-router-contracts/contracts/interfaces/IQuoterV2.sol
Normal file
@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @title QuoterV2 Interface
|
||||
/// @notice Supports quoting the calculated amounts from exact input or exact output swaps.
|
||||
/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap.
|
||||
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting
|
||||
/// to compute the result. They are also not gas efficient and should not be called on-chain.
|
||||
interface IQuoterV2 {
|
||||
/// @notice Returns the amount out received for a given exact input swap without executing the swap
|
||||
/// @param path The path of the swap, i.e. each token pair and the pool fee
|
||||
/// @param amountIn The amount of the first token to swap
|
||||
/// @return amountOut The amount of the last token that would be received
|
||||
/// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
|
||||
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
|
||||
/// @return gasEstimate The estimate of the gas that the swap consumes
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn)
|
||||
external
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160[] memory sqrtPriceX96AfterList,
|
||||
uint32[] memory initializedTicksCrossedList,
|
||||
uint256 gasEstimate
|
||||
);
|
||||
|
||||
struct QuoteExactInputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint256 amountIn;
|
||||
uint24 fee;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
/// @notice Returns the amount out received for a given exact input but for a swap of a single pool
|
||||
/// @param params The params for the quote, encoded as `QuoteExactInputSingleParams`
|
||||
/// tokenIn The token being swapped in
|
||||
/// tokenOut The token being swapped out
|
||||
/// fee The fee of the token pool to consider for the pair
|
||||
/// amountIn The desired input amount
|
||||
/// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap
|
||||
/// @return amountOut The amount of `tokenOut` that would be received
|
||||
/// @return sqrtPriceX96After The sqrt price of the pool after the swap
|
||||
/// @return initializedTicksCrossed The number of initialized ticks that the swap crossed
|
||||
/// @return gasEstimate The estimate of the gas that the swap consumes
|
||||
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
|
||||
external
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
);
|
||||
|
||||
/// @notice Returns the amount in required for a given exact output swap without executing the swap
|
||||
/// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
|
||||
/// @param amountOut The amount of the last token to receive
|
||||
/// @return amountIn The amount of first token required to be paid
|
||||
/// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
|
||||
/// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
|
||||
/// @return gasEstimate The estimate of the gas that the swap consumes
|
||||
function quoteExactOutput(bytes memory path, uint256 amountOut)
|
||||
external
|
||||
returns (
|
||||
uint256 amountIn,
|
||||
uint160[] memory sqrtPriceX96AfterList,
|
||||
uint32[] memory initializedTicksCrossedList,
|
||||
uint256 gasEstimate
|
||||
);
|
||||
|
||||
struct QuoteExactOutputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint256 amount;
|
||||
uint24 fee;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
/// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool
|
||||
/// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams`
|
||||
/// tokenIn The token being swapped in
|
||||
/// tokenOut The token being swapped out
|
||||
/// fee The fee of the token pool to consider for the pair
|
||||
/// amountOut The desired output amount
|
||||
/// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap
|
||||
/// @return amountIn The amount required as the input for the swap in order to receive `amountOut`
|
||||
/// @return sqrtPriceX96After The sqrt price of the pool after the swap
|
||||
/// @return initializedTicksCrossed The number of initialized ticks that the swap crossed
|
||||
/// @return gasEstimate The estimate of the gas that the swap consumes
|
||||
function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)
|
||||
external
|
||||
returns (
|
||||
uint256 amountIn,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/interfaces/ISelfPermit.sol';
|
||||
|
||||
import './IV2SwapRouter.sol';
|
||||
import './IV3SwapRouter.sol';
|
||||
import './IApproveAndCall.sol';
|
||||
import './IMulticallExtended.sol';
|
||||
|
||||
/// @title Router token swapping functionality
|
||||
interface ISwapRouter02 is IV2SwapRouter, IV3SwapRouter, IApproveAndCall, IMulticallExtended, ISelfPermit {
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @notice Validates tokens by flash borrowing from the token/<base token> pool on V2.
|
||||
/// @notice Returns
|
||||
/// Status.FOT if we detected a fee is taken on transfer.
|
||||
/// Status.STF if transfer failed for the token.
|
||||
/// Status.UNKN if we did not detect any issues with the token.
|
||||
/// @notice A return value of Status.UNKN does not mean the token is definitely not a fee on transfer token
|
||||
/// or definitely has no problems with its transfer. It just means we cant say for sure that it has any
|
||||
/// issues.
|
||||
/// @dev We can not guarantee the result of this lens is correct for a few reasons:
|
||||
/// @dev 1/ Some tokens take fees or allow transfers under specific conditions, for example some have an allowlist
|
||||
/// @dev of addresses that do/dont require fees. Therefore the result is not guaranteed to be correct
|
||||
/// @dev in all circumstances.
|
||||
/// @dev 2/ It is possible that the token does not have any pools on V2 therefore we are not able to perform
|
||||
/// @dev a flashloan to test the token.
|
||||
/// @dev These functions are not marked view because they rely on calling non-view functions and reverting
|
||||
/// to compute the result.
|
||||
interface ITokenValidator {
|
||||
// Status.FOT: detected a fee is taken on transfer.
|
||||
// Status.STF: transfer failed for the token.
|
||||
// Status.UNKN: no issues found with the token.
|
||||
enum Status {UNKN, FOT, STF}
|
||||
|
||||
/// @notice Validates a token by detecting if its transferable or takes a fee on transfer
|
||||
/// @param token The address of the token to check for fee on transfer
|
||||
/// @param baseTokens The addresses of the tokens to try pairing with
|
||||
/// token when looking for a pool to flash loan from.
|
||||
/// @param amountToBorrow The amount to try flash borrowing from the pools
|
||||
/// @return The status of the token
|
||||
function validate(
|
||||
address token,
|
||||
address[] calldata baseTokens,
|
||||
uint256 amountToBorrow
|
||||
) external returns (Status);
|
||||
|
||||
/// @notice Validates each token by detecting if its transferable or takes a fee on transfer
|
||||
/// @param tokens The addresses of the tokens to check for fee on transfer
|
||||
/// @param baseTokens The addresses of the tokens to try pairing with
|
||||
/// token when looking for a pool to flash loan from.
|
||||
/// @param amountToBorrow The amount to try flash borrowing from the pools
|
||||
/// @return The status of the token
|
||||
function batchValidate(
|
||||
address[] calldata tokens,
|
||||
address[] calldata baseTokens,
|
||||
uint256 amountToBorrow
|
||||
) external returns (Status[] memory);
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
/// @title Router token swapping functionality
|
||||
/// @notice Functions for swapping tokens via Uniswap V2
|
||||
interface IV2SwapRouter {
|
||||
/// @notice Swaps `amountIn` of one token for as much as possible of another token
|
||||
/// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
|
||||
/// and swap the entire amount, enabling contracts to send tokens before calling this function.
|
||||
/// @param amountIn The amount of token to swap
|
||||
/// @param amountOutMin The minimum amount of output that must be received
|
||||
/// @param path The ordered list of tokens to swap through
|
||||
/// @param to The recipient address
|
||||
/// @return amountOut The amount of the received token
|
||||
function swapExactTokensForTokens(
|
||||
uint256 amountIn,
|
||||
uint256 amountOutMin,
|
||||
address[] calldata path,
|
||||
address to
|
||||
) external payable returns (uint256 amountOut);
|
||||
|
||||
/// @notice Swaps as little as possible of one token for an exact amount of another token
|
||||
/// @param amountOut The amount of token to swap for
|
||||
/// @param amountInMax The maximum amount of input that the caller will pay
|
||||
/// @param path The ordered list of tokens to swap through
|
||||
/// @param to The recipient address
|
||||
/// @return amountIn The amount of token to pay
|
||||
function swapTokensForExactTokens(
|
||||
uint256 amountOut,
|
||||
uint256 amountInMax,
|
||||
address[] calldata path,
|
||||
address to
|
||||
) external payable returns (uint256 amountIn);
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.7.5;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
|
||||
|
||||
/// @title Router token swapping functionality
|
||||
/// @notice Functions for swapping tokens via Uniswap V3
|
||||
interface IV3SwapRouter is IUniswapV3SwapCallback {
|
||||
struct ExactInputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint24 fee;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
/// @notice Swaps `amountIn` of one token for as much as possible of another token
|
||||
/// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
|
||||
/// and swap the entire amount, enabling contracts to send tokens before calling this function.
|
||||
/// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata
|
||||
/// @return amountOut The amount of the received token
|
||||
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
|
||||
|
||||
struct ExactInputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountIn;
|
||||
uint256 amountOutMinimum;
|
||||
}
|
||||
|
||||
/// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path
|
||||
/// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
|
||||
/// and swap the entire amount, enabling contracts to send tokens before calling this function.
|
||||
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata
|
||||
/// @return amountOut The amount of the received token
|
||||
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
|
||||
|
||||
struct ExactOutputSingleParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
uint24 fee;
|
||||
address recipient;
|
||||
uint256 amountOut;
|
||||
uint256 amountInMaximum;
|
||||
uint160 sqrtPriceLimitX96;
|
||||
}
|
||||
|
||||
/// @notice Swaps as little as possible of one token for `amountOut` of another token
|
||||
/// that may remain in the router after the swap.
|
||||
/// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata
|
||||
/// @return amountIn The amount of the input token
|
||||
function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
|
||||
|
||||
struct ExactOutputParams {
|
||||
bytes path;
|
||||
address recipient;
|
||||
uint256 amountOut;
|
||||
uint256 amountInMaximum;
|
||||
}
|
||||
|
||||
/// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed)
|
||||
/// that may remain in the router after the swap.
|
||||
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata
|
||||
/// @return amountIn The amount of the input token
|
||||
function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);
|
||||
}
|
||||
10
lib/swap-router-contracts/contracts/interfaces/IWETH.sol
Normal file
10
lib/swap-router-contracts/contracts/interfaces/IWETH.sol
Normal file
@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.5.0;
|
||||
|
||||
interface IWETH {
|
||||
function deposit() external payable;
|
||||
|
||||
function transfer(address to, uint256 value) external returns (bool);
|
||||
|
||||
function withdraw(uint256) external;
|
||||
}
|
||||
237
lib/swap-router-contracts/contracts/lens/MixedRouteQuoterV1.sol
Normal file
237
lib/swap-router-contracts/contracts/lens/MixedRouteQuoterV1.sol
Normal file
@ -0,0 +1,237 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickBitmap.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
|
||||
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
|
||||
|
||||
import '../base/ImmutableState.sol';
|
||||
import '../interfaces/IMixedRouteQuoterV1.sol';
|
||||
import '../libraries/PoolTicksCounter.sol';
|
||||
import '../libraries/UniswapV2Library.sol';
|
||||
|
||||
/// @title Provides on chain quotes for V3, V2, and MixedRoute exact input swaps
|
||||
/// @notice Allows getting the expected amount out for a given swap without executing the swap
|
||||
/// @notice Does not support exact output swaps since using the contract balance between exactOut swaps is not supported
|
||||
/// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute
|
||||
/// the swap and check the amounts in the callback.
|
||||
contract MixedRouteQuoterV1 is IMixedRouteQuoterV1, IUniswapV3SwapCallback, PeripheryImmutableState {
|
||||
using Path for bytes;
|
||||
using SafeCast for uint256;
|
||||
using PoolTicksCounter for IUniswapV3Pool;
|
||||
address public immutable factoryV2;
|
||||
/// @dev Value to bit mask with path fee to determine if V2 or V3 route
|
||||
// max V3 fee: 000011110100001001000000 (24 bits)
|
||||
// mask: 1 << 23 = 100000000000000000000000 = decimal value 8388608
|
||||
uint24 private constant flagBitmask = 8388608;
|
||||
|
||||
/// @dev Transient storage variable used to check a safety condition in exact output swaps.
|
||||
uint256 private amountOutCached;
|
||||
|
||||
constructor(
|
||||
address _factory,
|
||||
address _factoryV2,
|
||||
address _WETH9
|
||||
) PeripheryImmutableState(_factory, _WETH9) {
|
||||
factoryV2 = _factoryV2;
|
||||
}
|
||||
|
||||
function getPool(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) private view returns (IUniswapV3Pool) {
|
||||
return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
||||
}
|
||||
|
||||
/// @dev Given an amountIn, fetch the reserves of the V2 pair and get the amountOut
|
||||
function getPairAmountOut(
|
||||
uint256 amountIn,
|
||||
address tokenIn,
|
||||
address tokenOut
|
||||
) private view returns (uint256) {
|
||||
(uint256 reserveIn, uint256 reserveOut) = UniswapV2Library.getReserves(factoryV2, tokenIn, tokenOut);
|
||||
return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3SwapCallback
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes memory path
|
||||
) external view override {
|
||||
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
|
||||
|
||||
(bool isExactInput, uint256 amountReceived) =
|
||||
amount0Delta > 0
|
||||
? (tokenIn < tokenOut, uint256(-amount1Delta))
|
||||
: (tokenOut < tokenIn, uint256(-amount0Delta));
|
||||
|
||||
IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee);
|
||||
(uint160 v3SqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0();
|
||||
|
||||
if (isExactInput) {
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, amountReceived)
|
||||
mstore(add(ptr, 0x20), v3SqrtPriceX96After)
|
||||
mstore(add(ptr, 0x40), tickAfter)
|
||||
revert(ptr, 0x60)
|
||||
}
|
||||
} else {
|
||||
/// since we don't support exactOutput, revert here
|
||||
revert('Exact output quote not supported');
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Parses a revert reason that should contain the numeric quote
|
||||
function parseRevertReason(bytes memory reason)
|
||||
private
|
||||
pure
|
||||
returns (
|
||||
uint256 amount,
|
||||
uint160 sqrtPriceX96After,
|
||||
int24 tickAfter
|
||||
)
|
||||
{
|
||||
if (reason.length != 0x60) {
|
||||
if (reason.length < 0x44) revert('Unexpected error');
|
||||
assembly {
|
||||
reason := add(reason, 0x04)
|
||||
}
|
||||
revert(abi.decode(reason, (string)));
|
||||
}
|
||||
return abi.decode(reason, (uint256, uint160, int24));
|
||||
}
|
||||
|
||||
function handleV3Revert(
|
||||
bytes memory reason,
|
||||
IUniswapV3Pool pool,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
private
|
||||
view
|
||||
returns (
|
||||
uint256 amount,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256
|
||||
)
|
||||
{
|
||||
int24 tickBefore;
|
||||
int24 tickAfter;
|
||||
(, tickBefore, , , , , ) = pool.slot0();
|
||||
(amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason);
|
||||
|
||||
initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter);
|
||||
|
||||
return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate);
|
||||
}
|
||||
|
||||
/// @dev Fetch an exactIn quote for a V3 Pool on chain
|
||||
function quoteExactInputSingleV3(QuoteExactInputSingleV3Params memory params)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
{
|
||||
bool zeroForOne = params.tokenIn < params.tokenOut;
|
||||
IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);
|
||||
|
||||
uint256 gasBefore = gasleft();
|
||||
try
|
||||
pool.swap(
|
||||
address(this), // address(0) might cause issues with some tokens
|
||||
zeroForOne,
|
||||
params.amountIn.toInt256(),
|
||||
params.sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: params.sqrtPriceLimitX96,
|
||||
abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
|
||||
)
|
||||
{} catch (bytes memory reason) {
|
||||
gasEstimate = gasBefore - gasleft();
|
||||
return handleV3Revert(reason, pool, gasEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Fetch an exactIn quote for a V2 pair on chain
|
||||
function quoteExactInputSingleV2(QuoteExactInputSingleV2Params memory params)
|
||||
public
|
||||
view
|
||||
override
|
||||
returns (uint256 amountOut)
|
||||
{
|
||||
amountOut = getPairAmountOut(params.amountIn, params.tokenIn, params.tokenOut);
|
||||
}
|
||||
|
||||
/// @dev Get the quote for an exactIn swap between an array of V2 and/or V3 pools
|
||||
/// @notice To encode a V2 pair within the path, use 0x800000 (hex value of 8388608) for the fee between the two token addresses
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160[] memory v3SqrtPriceX96AfterList,
|
||||
uint32[] memory v3InitializedTicksCrossedList,
|
||||
uint256 v3SwapGasEstimate
|
||||
)
|
||||
{
|
||||
v3SqrtPriceX96AfterList = new uint160[](path.numPools());
|
||||
v3InitializedTicksCrossedList = new uint32[](path.numPools());
|
||||
|
||||
uint256 i = 0;
|
||||
while (true) {
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
|
||||
if (fee & flagBitmask != 0) {
|
||||
amountIn = quoteExactInputSingleV2(
|
||||
QuoteExactInputSingleV2Params({tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn})
|
||||
);
|
||||
} else {
|
||||
/// the outputs of prior swaps become the inputs to subsequent ones
|
||||
(
|
||||
uint256 _amountOut,
|
||||
uint160 _sqrtPriceX96After,
|
||||
uint32 _initializedTicksCrossed,
|
||||
uint256 _gasEstimate
|
||||
) =
|
||||
quoteExactInputSingleV3(
|
||||
QuoteExactInputSingleV3Params({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
fee: fee,
|
||||
amountIn: amountIn,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
v3SqrtPriceX96AfterList[i] = _sqrtPriceX96After;
|
||||
v3InitializedTicksCrossedList[i] = _initializedTicksCrossed;
|
||||
v3SwapGasEstimate += _gasEstimate;
|
||||
amountIn = _amountOut;
|
||||
}
|
||||
i++;
|
||||
|
||||
/// decide whether to continue or terminate
|
||||
if (path.hasMultiplePools()) {
|
||||
path = path.skipToken();
|
||||
} else {
|
||||
return (amountIn, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList, v3SwapGasEstimate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
lib/swap-router-contracts/contracts/lens/Quoter.sol
Normal file
170
lib/swap-router-contracts/contracts/lens/Quoter.sol
Normal file
@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
|
||||
|
||||
import '../interfaces/IQuoter.sol';
|
||||
|
||||
/// @title Provides quotes for swaps
|
||||
/// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap
|
||||
/// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute
|
||||
/// the swap and check the amounts in the callback.
|
||||
contract Quoter is IQuoter, IUniswapV3SwapCallback, PeripheryImmutableState {
|
||||
using Path for bytes;
|
||||
using SafeCast for uint256;
|
||||
|
||||
/// @dev Transient storage variable used to check a safety condition in exact output swaps.
|
||||
uint256 private amountOutCached;
|
||||
|
||||
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
|
||||
|
||||
function getPool(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) private view returns (IUniswapV3Pool) {
|
||||
return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3SwapCallback
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes memory path
|
||||
) external view override {
|
||||
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
|
||||
|
||||
(bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
|
||||
amount0Delta > 0
|
||||
? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
|
||||
: (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
|
||||
if (isExactInput) {
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, amountReceived)
|
||||
revert(ptr, 32)
|
||||
}
|
||||
} else {
|
||||
// if the cache has been populated, ensure that the full output amount has been received
|
||||
if (amountOutCached != 0) require(amountReceived == amountOutCached);
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, amountToPay)
|
||||
revert(ptr, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Parses a revert reason that should contain the numeric quote
|
||||
function parseRevertReason(bytes memory reason) private pure returns (uint256) {
|
||||
if (reason.length != 32) {
|
||||
if (reason.length < 68) revert('Unexpected error');
|
||||
assembly {
|
||||
reason := add(reason, 0x04)
|
||||
}
|
||||
revert(abi.decode(reason, (string)));
|
||||
}
|
||||
return abi.decode(reason, (uint256));
|
||||
}
|
||||
|
||||
/// @inheritdoc IQuoter
|
||||
function quoteExactInputSingle(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountIn,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) public override returns (uint256 amountOut) {
|
||||
bool zeroForOne = tokenIn < tokenOut;
|
||||
|
||||
try
|
||||
getPool(tokenIn, tokenOut, fee).swap(
|
||||
address(this), // address(0) might cause issues with some tokens
|
||||
zeroForOne,
|
||||
amountIn.toInt256(),
|
||||
sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: sqrtPriceLimitX96,
|
||||
abi.encodePacked(tokenIn, fee, tokenOut)
|
||||
)
|
||||
{} catch (bytes memory reason) {
|
||||
return parseRevertReason(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IQuoter
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn) external override returns (uint256 amountOut) {
|
||||
while (true) {
|
||||
bool hasMultiplePools = path.hasMultiplePools();
|
||||
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
|
||||
// the outputs of prior swaps become the inputs to subsequent ones
|
||||
amountIn = quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0);
|
||||
|
||||
// decide whether to continue or terminate
|
||||
if (hasMultiplePools) {
|
||||
path = path.skipToken();
|
||||
} else {
|
||||
return amountIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IQuoter
|
||||
function quoteExactOutputSingle(
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee,
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) public override returns (uint256 amountIn) {
|
||||
bool zeroForOne = tokenIn < tokenOut;
|
||||
|
||||
// if no price limit has been specified, cache the output amount for comparison in the swap callback
|
||||
if (sqrtPriceLimitX96 == 0) amountOutCached = amountOut;
|
||||
try
|
||||
getPool(tokenIn, tokenOut, fee).swap(
|
||||
address(this), // address(0) might cause issues with some tokens
|
||||
zeroForOne,
|
||||
-amountOut.toInt256(),
|
||||
sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: sqrtPriceLimitX96,
|
||||
abi.encodePacked(tokenOut, fee, tokenIn)
|
||||
)
|
||||
{} catch (bytes memory reason) {
|
||||
if (sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache
|
||||
return parseRevertReason(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// @inheritdoc IQuoter
|
||||
function quoteExactOutput(bytes memory path, uint256 amountOut) external override returns (uint256 amountIn) {
|
||||
while (true) {
|
||||
bool hasMultiplePools = path.hasMultiplePools();
|
||||
|
||||
(address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();
|
||||
|
||||
// the inputs of prior swaps become the outputs of subsequent ones
|
||||
amountOut = quoteExactOutputSingle(tokenIn, tokenOut, fee, amountOut, 0);
|
||||
|
||||
// decide whether to continue or terminate
|
||||
if (hasMultiplePools) {
|
||||
path = path.skipToken();
|
||||
} else {
|
||||
return amountOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
273
lib/swap-router-contracts/contracts/lens/QuoterV2.sol
Normal file
273
lib/swap-router-contracts/contracts/lens/QuoterV2.sol
Normal file
@ -0,0 +1,273 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickMath.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/TickBitmap.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
||||
import '@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol';
|
||||
|
||||
import '../interfaces/IQuoterV2.sol';
|
||||
import '../libraries/PoolTicksCounter.sol';
|
||||
|
||||
/// @title Provides quotes for swaps
|
||||
/// @notice Allows getting the expected amount out or amount in for a given swap without executing the swap
|
||||
/// @dev These functions are not gas efficient and should _not_ be called on chain. Instead, optimistically execute
|
||||
/// the swap and check the amounts in the callback.
|
||||
contract QuoterV2 is IQuoterV2, IUniswapV3SwapCallback, PeripheryImmutableState {
|
||||
using Path for bytes;
|
||||
using SafeCast for uint256;
|
||||
using PoolTicksCounter for IUniswapV3Pool;
|
||||
|
||||
/// @dev Transient storage variable used to check a safety condition in exact output swaps.
|
||||
uint256 private amountOutCached;
|
||||
|
||||
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
|
||||
|
||||
function getPool(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) private view returns (IUniswapV3Pool) {
|
||||
return IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
||||
}
|
||||
|
||||
/// @inheritdoc IUniswapV3SwapCallback
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes memory path
|
||||
) external view override {
|
||||
require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
|
||||
|
||||
(bool isExactInput, uint256 amountToPay, uint256 amountReceived) =
|
||||
amount0Delta > 0
|
||||
? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta))
|
||||
: (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta));
|
||||
|
||||
IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee);
|
||||
(uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0();
|
||||
|
||||
if (isExactInput) {
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, amountReceived)
|
||||
mstore(add(ptr, 0x20), sqrtPriceX96After)
|
||||
mstore(add(ptr, 0x40), tickAfter)
|
||||
revert(ptr, 96)
|
||||
}
|
||||
} else {
|
||||
// if the cache has been populated, ensure that the full output amount has been received
|
||||
if (amountOutCached != 0) require(amountReceived == amountOutCached);
|
||||
assembly {
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, amountToPay)
|
||||
mstore(add(ptr, 0x20), sqrtPriceX96After)
|
||||
mstore(add(ptr, 0x40), tickAfter)
|
||||
revert(ptr, 96)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Parses a revert reason that should contain the numeric quote
|
||||
function parseRevertReason(bytes memory reason)
|
||||
private
|
||||
pure
|
||||
returns (
|
||||
uint256 amount,
|
||||
uint160 sqrtPriceX96After,
|
||||
int24 tickAfter
|
||||
)
|
||||
{
|
||||
if (reason.length != 96) {
|
||||
if (reason.length < 68) revert('Unexpected error');
|
||||
assembly {
|
||||
reason := add(reason, 0x04)
|
||||
}
|
||||
revert(abi.decode(reason, (string)));
|
||||
}
|
||||
return abi.decode(reason, (uint256, uint160, int24));
|
||||
}
|
||||
|
||||
function handleRevert(
|
||||
bytes memory reason,
|
||||
IUniswapV3Pool pool,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
private
|
||||
view
|
||||
returns (
|
||||
uint256 amount,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256
|
||||
)
|
||||
{
|
||||
int24 tickBefore;
|
||||
int24 tickAfter;
|
||||
(, tickBefore, , , , , ) = pool.slot0();
|
||||
(amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason);
|
||||
|
||||
initializedTicksCrossed = pool.countInitializedTicksCrossed(tickBefore, tickAfter);
|
||||
|
||||
return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate);
|
||||
}
|
||||
|
||||
function quoteExactInputSingle(QuoteExactInputSingleParams memory params)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
{
|
||||
bool zeroForOne = params.tokenIn < params.tokenOut;
|
||||
IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);
|
||||
|
||||
uint256 gasBefore = gasleft();
|
||||
try
|
||||
pool.swap(
|
||||
address(this), // address(0) might cause issues with some tokens
|
||||
zeroForOne,
|
||||
params.amountIn.toInt256(),
|
||||
params.sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: params.sqrtPriceLimitX96,
|
||||
abi.encodePacked(params.tokenIn, params.fee, params.tokenOut)
|
||||
)
|
||||
{} catch (bytes memory reason) {
|
||||
gasEstimate = gasBefore - gasleft();
|
||||
return handleRevert(reason, pool, gasEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
function quoteExactInput(bytes memory path, uint256 amountIn)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountOut,
|
||||
uint160[] memory sqrtPriceX96AfterList,
|
||||
uint32[] memory initializedTicksCrossedList,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
{
|
||||
sqrtPriceX96AfterList = new uint160[](path.numPools());
|
||||
initializedTicksCrossedList = new uint32[](path.numPools());
|
||||
|
||||
uint256 i = 0;
|
||||
while (true) {
|
||||
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
||||
|
||||
// the outputs of prior swaps become the inputs to subsequent ones
|
||||
(uint256 _amountOut, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
|
||||
quoteExactInputSingle(
|
||||
QuoteExactInputSingleParams({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
fee: fee,
|
||||
amountIn: amountIn,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
|
||||
sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
|
||||
initializedTicksCrossedList[i] = _initializedTicksCrossed;
|
||||
amountIn = _amountOut;
|
||||
gasEstimate += _gasEstimate;
|
||||
i++;
|
||||
|
||||
// decide whether to continue or terminate
|
||||
if (path.hasMultiplePools()) {
|
||||
path = path.skipToken();
|
||||
} else {
|
||||
return (amountIn, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountIn,
|
||||
uint160 sqrtPriceX96After,
|
||||
uint32 initializedTicksCrossed,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
{
|
||||
bool zeroForOne = params.tokenIn < params.tokenOut;
|
||||
IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee);
|
||||
|
||||
// if no price limit has been specified, cache the output amount for comparison in the swap callback
|
||||
if (params.sqrtPriceLimitX96 == 0) amountOutCached = params.amount;
|
||||
uint256 gasBefore = gasleft();
|
||||
try
|
||||
pool.swap(
|
||||
address(this), // address(0) might cause issues with some tokens
|
||||
zeroForOne,
|
||||
-params.amount.toInt256(),
|
||||
params.sqrtPriceLimitX96 == 0
|
||||
? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1)
|
||||
: params.sqrtPriceLimitX96,
|
||||
abi.encodePacked(params.tokenOut, params.fee, params.tokenIn)
|
||||
)
|
||||
{} catch (bytes memory reason) {
|
||||
gasEstimate = gasBefore - gasleft();
|
||||
if (params.sqrtPriceLimitX96 == 0) delete amountOutCached; // clear cache
|
||||
return handleRevert(reason, pool, gasEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
function quoteExactOutput(bytes memory path, uint256 amountOut)
|
||||
public
|
||||
override
|
||||
returns (
|
||||
uint256 amountIn,
|
||||
uint160[] memory sqrtPriceX96AfterList,
|
||||
uint32[] memory initializedTicksCrossedList,
|
||||
uint256 gasEstimate
|
||||
)
|
||||
{
|
||||
sqrtPriceX96AfterList = new uint160[](path.numPools());
|
||||
initializedTicksCrossedList = new uint32[](path.numPools());
|
||||
|
||||
uint256 i = 0;
|
||||
while (true) {
|
||||
(address tokenOut, address tokenIn, uint24 fee) = path.decodeFirstPool();
|
||||
|
||||
// the inputs of prior swaps become the outputs of subsequent ones
|
||||
(uint256 _amountIn, uint160 _sqrtPriceX96After, uint32 _initializedTicksCrossed, uint256 _gasEstimate) =
|
||||
quoteExactOutputSingle(
|
||||
QuoteExactOutputSingleParams({
|
||||
tokenIn: tokenIn,
|
||||
tokenOut: tokenOut,
|
||||
amount: amountOut,
|
||||
fee: fee,
|
||||
sqrtPriceLimitX96: 0
|
||||
})
|
||||
);
|
||||
|
||||
sqrtPriceX96AfterList[i] = _sqrtPriceX96After;
|
||||
initializedTicksCrossedList[i] = _initializedTicksCrossed;
|
||||
amountOut = _amountIn;
|
||||
gasEstimate += _gasEstimate;
|
||||
i++;
|
||||
|
||||
// decide whether to continue or terminate
|
||||
if (path.hasMultiplePools()) {
|
||||
path = path.skipToken();
|
||||
} else {
|
||||
return (amountOut, sqrtPriceX96AfterList, initializedTicksCrossedList, gasEstimate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
lib/swap-router-contracts/contracts/lens/README.md
Normal file
4
lib/swap-router-contracts/contracts/lens/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# lens
|
||||
|
||||
These contracts are not designed to be called on-chain. They simplify
|
||||
fetching on-chain data from off-chain.
|
||||
159
lib/swap-router-contracts/contracts/lens/TokenValidator.sol
Normal file
159
lib/swap-router-contracts/contracts/lens/TokenValidator.sol
Normal file
@ -0,0 +1,159 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
||||
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Callee.sol';
|
||||
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
|
||||
import '../libraries/UniswapV2Library.sol';
|
||||
import '../interfaces/ISwapRouter02.sol';
|
||||
import '../interfaces/ITokenValidator.sol';
|
||||
import '../base/ImmutableState.sol';
|
||||
|
||||
/// @notice Validates tokens by flash borrowing from the token/<base token> pool on V2.
|
||||
/// @notice Returns
|
||||
/// Status.FOT if we detected a fee is taken on transfer.
|
||||
/// Status.STF if transfer failed for the token.
|
||||
/// Status.UNKN if we did not detect any issues with the token.
|
||||
/// @notice A return value of Status.UNKN does not mean the token is definitely not a fee on transfer token
|
||||
/// or definitely has no problems with its transfer. It just means we cant say for sure that it has any
|
||||
/// issues.
|
||||
/// @dev We can not guarantee the result of this lens is correct for a few reasons:
|
||||
/// @dev 1/ Some tokens take fees or allow transfers under specific conditions, for example some have an allowlist
|
||||
/// @dev of addresses that do/dont require fees. Therefore the result is not guaranteed to be correct
|
||||
/// @dev in all circumstances.
|
||||
/// @dev 2/ It is possible that the token does not have any pools on V2 therefore we are not able to perform
|
||||
/// @dev a flashloan to test the token.
|
||||
contract TokenValidator is ITokenValidator, IUniswapV2Callee, ImmutableState {
|
||||
string internal constant FOT_REVERT_STRING = 'FOT';
|
||||
// https://github.com/Uniswap/v2-core/blob/1136544ac842ff48ae0b1b939701436598d74075/contracts/UniswapV2Pair.sol#L46
|
||||
string internal constant STF_REVERT_STRING_SUFFIX = 'TRANSFER_FAILED';
|
||||
|
||||
constructor(address _factoryV2, address _positionManager) ImmutableState(_factoryV2, _positionManager) {}
|
||||
|
||||
function batchValidate(
|
||||
address[] calldata tokens,
|
||||
address[] calldata baseTokens,
|
||||
uint256 amountToBorrow
|
||||
) public override returns (Status[] memory isFotResults) {
|
||||
isFotResults = new Status[](tokens.length);
|
||||
for (uint256 i = 0; i < tokens.length; i++) {
|
||||
isFotResults[i] = validate(tokens[i], baseTokens, amountToBorrow);
|
||||
}
|
||||
}
|
||||
|
||||
function validate(
|
||||
address token,
|
||||
address[] calldata baseTokens,
|
||||
uint256 amountToBorrow
|
||||
) public override returns (Status) {
|
||||
for (uint256 i = 0; i < baseTokens.length; i++) {
|
||||
Status result = _validate(token, baseTokens[i], amountToBorrow);
|
||||
if (result == Status.FOT || result == Status.STF) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return Status.UNKN;
|
||||
}
|
||||
|
||||
function _validate(
|
||||
address token,
|
||||
address baseToken,
|
||||
uint256 amountToBorrow
|
||||
) internal returns (Status) {
|
||||
if (token == baseToken) {
|
||||
return Status.UNKN;
|
||||
}
|
||||
|
||||
address pairAddress = UniswapV2Library.pairFor(this.factoryV2(), token, baseToken);
|
||||
|
||||
// If the token/baseToken pair exists, get token0.
|
||||
// Must do low level call as try/catch does not support case where contract does not exist.
|
||||
(, bytes memory returnData) = address(pairAddress).call(abi.encodeWithSelector(IUniswapV2Pair.token0.selector));
|
||||
|
||||
if (returnData.length == 0) {
|
||||
return Status.UNKN;
|
||||
}
|
||||
|
||||
address token0Address = abi.decode(returnData, (address));
|
||||
|
||||
// Flash loan {amountToBorrow}
|
||||
(uint256 amount0Out, uint256 amount1Out) =
|
||||
token == token0Address ? (amountToBorrow, uint256(0)) : (uint256(0), amountToBorrow);
|
||||
|
||||
uint256 balanceBeforeLoan = IERC20(token).balanceOf(address(this));
|
||||
|
||||
IUniswapV2Pair pair = IUniswapV2Pair(pairAddress);
|
||||
|
||||
try
|
||||
pair.swap(amount0Out, amount1Out, address(this), abi.encode(balanceBeforeLoan, amountToBorrow))
|
||||
{} catch Error(string memory reason) {
|
||||
if (isFotFailed(reason)) {
|
||||
return Status.FOT;
|
||||
}
|
||||
|
||||
if (isTransferFailed(reason)) {
|
||||
return Status.STF;
|
||||
}
|
||||
|
||||
return Status.UNKN;
|
||||
}
|
||||
|
||||
// Swap always reverts so should never reach.
|
||||
revert('Unexpected error');
|
||||
}
|
||||
|
||||
function isFotFailed(string memory reason) internal pure returns (bool) {
|
||||
return keccak256(bytes(reason)) == keccak256(bytes(FOT_REVERT_STRING));
|
||||
}
|
||||
|
||||
function isTransferFailed(string memory reason) internal pure returns (bool) {
|
||||
// We check the suffix of the revert string so we can support forks that
|
||||
// may have modified the prefix.
|
||||
string memory stf = STF_REVERT_STRING_SUFFIX;
|
||||
|
||||
uint256 reasonLength = bytes(reason).length;
|
||||
uint256 suffixLength = bytes(stf).length;
|
||||
if (reasonLength < suffixLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint256 ptr;
|
||||
uint256 offset = 32 + reasonLength - suffixLength;
|
||||
bool transferFailed;
|
||||
assembly {
|
||||
ptr := add(reason, offset)
|
||||
let suffixPtr := add(stf, 32)
|
||||
transferFailed := eq(keccak256(ptr, suffixLength), keccak256(suffixPtr, suffixLength))
|
||||
}
|
||||
|
||||
return transferFailed;
|
||||
}
|
||||
|
||||
function uniswapV2Call(
|
||||
address,
|
||||
uint256 amount0,
|
||||
uint256,
|
||||
bytes calldata data
|
||||
) external view override {
|
||||
IUniswapV2Pair pair = IUniswapV2Pair(msg.sender);
|
||||
(address token0, address token1) = (pair.token0(), pair.token1());
|
||||
|
||||
IERC20 tokenBorrowed = IERC20(amount0 > 0 ? token0 : token1);
|
||||
|
||||
(uint256 balanceBeforeLoan, uint256 amountRequestedToBorrow) = abi.decode(data, (uint256, uint256));
|
||||
uint256 amountBorrowed = tokenBorrowed.balanceOf(address(this)) - balanceBeforeLoan;
|
||||
|
||||
// If we received less token than we requested when we called swap, then a fee must have been taken
|
||||
// by the token during transfer.
|
||||
if (amountBorrowed != amountRequestedToBorrow) {
|
||||
revert(FOT_REVERT_STRING);
|
||||
}
|
||||
|
||||
// Note: If we do not revert here, we would end up reverting in the pair's swap method anyway
|
||||
// since for a flash borrow we need to transfer back the amount we borrowed + 0.3% fee, and we don't
|
||||
// have funds to cover the fee. Revert early here to save gas/time.
|
||||
revert('Unknown');
|
||||
}
|
||||
}
|
||||
15
lib/swap-router-contracts/contracts/libraries/Constants.sol
Normal file
15
lib/swap-router-contracts/contracts/libraries/Constants.sol
Normal file
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
/// @title Constant state
|
||||
/// @notice Constant state used by the swap router
|
||||
library Constants {
|
||||
/// @dev Used for identifying cases when this contract's balance of a token is to be used
|
||||
uint256 internal constant CONTRACT_BALANCE = 0;
|
||||
|
||||
/// @dev Used as a flag for identifying msg.sender, saves gas by sending more 0 bytes
|
||||
address internal constant MSG_SENDER = address(1);
|
||||
|
||||
/// @dev Used as a flag for identifying address(this), saves gas by sending more 0 bytes
|
||||
address internal constant ADDRESS_THIS = address(2);
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.6.0;
|
||||
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
|
||||
library PoolTicksCounter {
|
||||
/// @dev This function counts the number of initialized ticks that would incur a gas cost between tickBefore and tickAfter.
|
||||
/// When tickBefore and/or tickAfter themselves are initialized, the logic over whether we should count them depends on the
|
||||
/// direction of the swap. If we are swapping upwards (tickAfter > tickBefore) we don't want to count tickBefore but we do
|
||||
/// want to count tickAfter. The opposite is true if we are swapping downwards.
|
||||
function countInitializedTicksCrossed(
|
||||
IUniswapV3Pool self,
|
||||
int24 tickBefore,
|
||||
int24 tickAfter
|
||||
) internal view returns (uint32 initializedTicksCrossed) {
|
||||
int16 wordPosLower;
|
||||
int16 wordPosHigher;
|
||||
uint8 bitPosLower;
|
||||
uint8 bitPosHigher;
|
||||
bool tickBeforeInitialized;
|
||||
bool tickAfterInitialized;
|
||||
|
||||
{
|
||||
// Get the key and offset in the tick bitmap of the active tick before and after the swap.
|
||||
int16 wordPos = int16((tickBefore / self.tickSpacing()) >> 8);
|
||||
uint8 bitPos = uint8((tickBefore / self.tickSpacing()) % 256);
|
||||
|
||||
int16 wordPosAfter = int16((tickAfter / self.tickSpacing()) >> 8);
|
||||
uint8 bitPosAfter = uint8((tickAfter / self.tickSpacing()) % 256);
|
||||
|
||||
// In the case where tickAfter is initialized, we only want to count it if we are swapping downwards.
|
||||
// If the initializable tick after the swap is initialized, our original tickAfter is a
|
||||
// multiple of tick spacing, and we are swapping downwards we know that tickAfter is initialized
|
||||
// and we shouldn't count it.
|
||||
tickAfterInitialized =
|
||||
((self.tickBitmap(wordPosAfter) & (1 << bitPosAfter)) > 0) &&
|
||||
((tickAfter % self.tickSpacing()) == 0) &&
|
||||
(tickBefore > tickAfter);
|
||||
|
||||
// In the case where tickBefore is initialized, we only want to count it if we are swapping upwards.
|
||||
// Use the same logic as above to decide whether we should count tickBefore or not.
|
||||
tickBeforeInitialized =
|
||||
((self.tickBitmap(wordPos) & (1 << bitPos)) > 0) &&
|
||||
((tickBefore % self.tickSpacing()) == 0) &&
|
||||
(tickBefore < tickAfter);
|
||||
|
||||
if (wordPos < wordPosAfter || (wordPos == wordPosAfter && bitPos <= bitPosAfter)) {
|
||||
wordPosLower = wordPos;
|
||||
bitPosLower = bitPos;
|
||||
wordPosHigher = wordPosAfter;
|
||||
bitPosHigher = bitPosAfter;
|
||||
} else {
|
||||
wordPosLower = wordPosAfter;
|
||||
bitPosLower = bitPosAfter;
|
||||
wordPosHigher = wordPos;
|
||||
bitPosHigher = bitPos;
|
||||
}
|
||||
}
|
||||
|
||||
// Count the number of initialized ticks crossed by iterating through the tick bitmap.
|
||||
// Our first mask should include the lower tick and everything to its left.
|
||||
uint256 mask = type(uint256).max << bitPosLower;
|
||||
while (wordPosLower <= wordPosHigher) {
|
||||
// If we're on the final tick bitmap page, ensure we only count up to our
|
||||
// ending tick.
|
||||
if (wordPosLower == wordPosHigher) {
|
||||
mask = mask & (type(uint256).max >> (255 - bitPosHigher));
|
||||
}
|
||||
|
||||
uint256 masked = self.tickBitmap(wordPosLower) & mask;
|
||||
initializedTicksCrossed += countOneBits(masked);
|
||||
wordPosLower++;
|
||||
// Reset our mask so we consider all bits on the next iteration.
|
||||
mask = type(uint256).max;
|
||||
}
|
||||
|
||||
if (tickAfterInitialized) {
|
||||
initializedTicksCrossed -= 1;
|
||||
}
|
||||
|
||||
if (tickBeforeInitialized) {
|
||||
initializedTicksCrossed -= 1;
|
||||
}
|
||||
|
||||
return initializedTicksCrossed;
|
||||
}
|
||||
|
||||
function countOneBits(uint256 x) private pure returns (uint16) {
|
||||
uint16 bits = 0;
|
||||
while (x != 0) {
|
||||
bits++;
|
||||
x &= (x - 1);
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity >=0.5.0;
|
||||
|
||||
import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol';
|
||||
|
||||
library UniswapV2Library {
|
||||
using LowGasSafeMath for uint256;
|
||||
|
||||
// returns sorted token addresses, used to handle return values from pairs sorted in this order
|
||||
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
|
||||
require(tokenA != tokenB);
|
||||
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
|
||||
require(token0 != address(0));
|
||||
}
|
||||
|
||||
// calculates the CREATE2 address for a pair without making any external calls
|
||||
function pairFor(
|
||||
address factory,
|
||||
address tokenA,
|
||||
address tokenB
|
||||
) internal pure returns (address pair) {
|
||||
(address token0, address token1) = sortTokens(tokenA, tokenB);
|
||||
pair = address(
|
||||
uint256(
|
||||
keccak256(
|
||||
abi.encodePacked(
|
||||
hex'ff',
|
||||
factory,
|
||||
keccak256(abi.encodePacked(token0, token1)),
|
||||
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// fetches and sorts the reserves for a pair
|
||||
function getReserves(
|
||||
address factory,
|
||||
address tokenA,
|
||||
address tokenB
|
||||
) internal view returns (uint256 reserveA, uint256 reserveB) {
|
||||
(address token0, ) = sortTokens(tokenA, tokenB);
|
||||
(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
|
||||
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
|
||||
}
|
||||
|
||||
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
|
||||
function getAmountOut(
|
||||
uint256 amountIn,
|
||||
uint256 reserveIn,
|
||||
uint256 reserveOut
|
||||
) internal pure returns (uint256 amountOut) {
|
||||
require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT');
|
||||
require(reserveIn > 0 && reserveOut > 0);
|
||||
uint256 amountInWithFee = amountIn.mul(997);
|
||||
uint256 numerator = amountInWithFee.mul(reserveOut);
|
||||
uint256 denominator = reserveIn.mul(1000).add(amountInWithFee);
|
||||
amountOut = numerator / denominator;
|
||||
}
|
||||
|
||||
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
|
||||
function getAmountIn(
|
||||
uint256 amountOut,
|
||||
uint256 reserveIn,
|
||||
uint256 reserveOut
|
||||
) internal pure returns (uint256 amountIn) {
|
||||
require(amountOut > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
|
||||
require(reserveIn > 0 && reserveOut > 0);
|
||||
uint256 numerator = reserveIn.mul(amountOut).mul(1000);
|
||||
uint256 denominator = reserveOut.sub(amountOut).mul(997);
|
||||
amountIn = (numerator / denominator).add(1);
|
||||
}
|
||||
|
||||
// performs chained getAmountIn calculations on any number of pairs
|
||||
function getAmountsIn(
|
||||
address factory,
|
||||
uint256 amountOut,
|
||||
address[] memory path
|
||||
) internal view returns (uint256[] memory amounts) {
|
||||
require(path.length >= 2);
|
||||
amounts = new uint256[](path.length);
|
||||
amounts[amounts.length - 1] = amountOut;
|
||||
for (uint256 i = path.length - 1; i > 0; i--) {
|
||||
(uint256 reserveIn, uint256 reserveOut) = getReserves(factory, path[i - 1], path[i]);
|
||||
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '../base/ImmutableState.sol';
|
||||
|
||||
contract ImmutableStateTest is ImmutableState {
|
||||
constructor(address _factoryV2, address _positionManager) ImmutableState(_factoryV2, _positionManager) {}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '@uniswap/v3-core/contracts/libraries/Oracle.sol';
|
||||
|
||||
contract MockObservations {
|
||||
using Oracle for Oracle.Observation[65535];
|
||||
|
||||
// slot0
|
||||
int24 private slot0Tick;
|
||||
uint16 private slot0ObservationCardinality;
|
||||
uint16 private slot0ObservationIndex;
|
||||
|
||||
// observations
|
||||
Oracle.Observation[65535] public observations;
|
||||
|
||||
// block timestamps always monotonic increasing from 0, cumulative ticks are calculated automatically
|
||||
constructor(
|
||||
uint32[3] memory blockTimestamps,
|
||||
int24[3] memory ticks,
|
||||
bool mockLowObservationCardinality
|
||||
) {
|
||||
require(blockTimestamps[0] == 0, '0');
|
||||
require(blockTimestamps[1] > 0, '1');
|
||||
require(blockTimestamps[2] > blockTimestamps[1], '2');
|
||||
|
||||
int56 tickCumulative = 0;
|
||||
for (uint256 i = 0; i < blockTimestamps.length; i++) {
|
||||
if (i != 0) {
|
||||
int24 tick = ticks[i - 1];
|
||||
uint32 delta = blockTimestamps[i] - blockTimestamps[i - 1];
|
||||
tickCumulative += int56(tick) * delta;
|
||||
}
|
||||
observations[i] = Oracle.Observation({
|
||||
blockTimestamp: blockTimestamps[i],
|
||||
tickCumulative: tickCumulative,
|
||||
secondsPerLiquidityCumulativeX128: uint160(i),
|
||||
initialized: true
|
||||
});
|
||||
}
|
||||
slot0Tick = ticks[2];
|
||||
slot0ObservationCardinality = mockLowObservationCardinality ? 1 : 3;
|
||||
slot0ObservationIndex = 2;
|
||||
}
|
||||
|
||||
function slot0()
|
||||
external
|
||||
view
|
||||
returns (
|
||||
uint160,
|
||||
int24,
|
||||
uint16,
|
||||
uint16,
|
||||
uint16,
|
||||
uint8,
|
||||
bool
|
||||
)
|
||||
{
|
||||
return (0, slot0Tick, slot0ObservationIndex, slot0ObservationCardinality, 0, 0, false);
|
||||
}
|
||||
|
||||
function observe(uint32[] calldata secondsAgos)
|
||||
external
|
||||
view
|
||||
returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)
|
||||
{
|
||||
return
|
||||
observations.observe(
|
||||
observations[2].blockTimestamp,
|
||||
secondsAgos,
|
||||
slot0Tick,
|
||||
slot0ObservationIndex,
|
||||
0,
|
||||
slot0ObservationCardinality
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '../SwapRouter02.sol';
|
||||
|
||||
contract MockTimeSwapRouter02 is SwapRouter02 {
|
||||
uint256 time;
|
||||
|
||||
constructor(
|
||||
address _factoryV2,
|
||||
address factoryV3,
|
||||
address _positionManager,
|
||||
address _WETH9
|
||||
) SwapRouter02(_factoryV2, factoryV3, _positionManager, _WETH9) {}
|
||||
|
||||
function _blockTimestamp() internal view override returns (uint256) {
|
||||
return time;
|
||||
}
|
||||
|
||||
function setTime(uint256 _time) external {
|
||||
time = _time;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '../base/OracleSlippage.sol';
|
||||
|
||||
contract OracleSlippageTest is OracleSlippage {
|
||||
mapping(address => mapping(address => mapping(uint24 => IUniswapV3Pool))) private pools;
|
||||
uint256 internal time;
|
||||
|
||||
constructor(address _factory, address _WETH9) PeripheryImmutableState(_factory, _WETH9) {}
|
||||
|
||||
function setTime(uint256 _time) external {
|
||||
time = _time;
|
||||
}
|
||||
|
||||
function _blockTimestamp() internal view override returns (uint256) {
|
||||
return time;
|
||||
}
|
||||
|
||||
function registerPool(
|
||||
IUniswapV3Pool pool,
|
||||
address tokenIn,
|
||||
address tokenOut,
|
||||
uint24 fee
|
||||
) external {
|
||||
pools[tokenIn][tokenOut][fee] = pool;
|
||||
pools[tokenOut][tokenIn][fee] = pool;
|
||||
}
|
||||
|
||||
function getPoolAddress(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint24 fee
|
||||
) internal view override returns (IUniswapV3Pool pool) {
|
||||
pool = pools[tokenA][tokenB][fee];
|
||||
}
|
||||
|
||||
function testGetBlockStartingAndCurrentTick(IUniswapV3Pool pool)
|
||||
external
|
||||
view
|
||||
returns (int24 blockStartingTick, int24 currentTick)
|
||||
{
|
||||
return getBlockStartingAndCurrentTick(pool);
|
||||
}
|
||||
|
||||
function testGetSyntheticTicks(bytes memory path, uint32 secondsAgo)
|
||||
external
|
||||
view
|
||||
returns (int256 syntheticAverageTick, int256 syntheticCurrentTick)
|
||||
{
|
||||
return getSyntheticTicks(path, secondsAgo);
|
||||
}
|
||||
|
||||
function testGetSyntheticTicks(
|
||||
bytes[] memory paths,
|
||||
uint128[] memory amounts,
|
||||
uint32 secondsAgo
|
||||
) external view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) {
|
||||
return getSyntheticTicks(paths, amounts, secondsAgo);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
|
||||
pragma solidity >=0.6.0;
|
||||
|
||||
import '../libraries/PoolTicksCounter.sol';
|
||||
|
||||
contract PoolTicksCounterTest {
|
||||
using PoolTicksCounter for IUniswapV3Pool;
|
||||
|
||||
function countInitializedTicksCrossed(
|
||||
IUniswapV3Pool pool,
|
||||
int24 tickBefore,
|
||||
int24 tickAfter
|
||||
) external view returns (uint32 initializedTicksCrossed) {
|
||||
return pool.countInitializedTicksCrossed(tickBefore, tickAfter);
|
||||
}
|
||||
}
|
||||
10
lib/swap-router-contracts/contracts/test/TestERC20.sol
Normal file
10
lib/swap-router-contracts/contracts/test/TestERC20.sol
Normal file
@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '@openzeppelin/contracts/drafts/ERC20Permit.sol';
|
||||
|
||||
contract TestERC20 is ERC20Permit {
|
||||
constructor(uint256 amountToMint) ERC20('Test ERC20', 'TEST') ERC20Permit('Test ERC20') {
|
||||
_mint(msg.sender, amountToMint);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import '../base/MulticallExtended.sol';
|
||||
|
||||
contract TestMulticallExtended is MulticallExtended {
|
||||
uint256 time;
|
||||
|
||||
function _blockTimestamp() internal view override returns (uint256) {
|
||||
return time;
|
||||
}
|
||||
|
||||
function setTime(uint256 _time) external {
|
||||
time = _time;
|
||||
}
|
||||
|
||||
struct Tuple {
|
||||
uint256 a;
|
||||
uint256 b;
|
||||
}
|
||||
|
||||
function functionThatReturnsTuple(uint256 a, uint256 b) external pure returns (Tuple memory tuple) {
|
||||
tuple = Tuple({b: a, a: b});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import '@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
|
||||
import '@uniswap/v3-core/contracts/libraries/SafeCast.sol';
|
||||
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
|
||||
contract TestUniswapV3Callee is IUniswapV3SwapCallback {
|
||||
using SafeCast for uint256;
|
||||
|
||||
function swapExact0For1(
|
||||
address pool,
|
||||
uint256 amount0In,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external {
|
||||
IUniswapV3Pool(pool).swap(recipient, true, amount0In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender));
|
||||
}
|
||||
|
||||
function swap0ForExact1(
|
||||
address pool,
|
||||
uint256 amount1Out,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external {
|
||||
IUniswapV3Pool(pool).swap(recipient, true, -amount1Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender));
|
||||
}
|
||||
|
||||
function swapExact1For0(
|
||||
address pool,
|
||||
uint256 amount1In,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external {
|
||||
IUniswapV3Pool(pool).swap(recipient, false, amount1In.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender));
|
||||
}
|
||||
|
||||
function swap1ForExact0(
|
||||
address pool,
|
||||
uint256 amount0Out,
|
||||
address recipient,
|
||||
uint160 sqrtPriceLimitX96
|
||||
) external {
|
||||
IUniswapV3Pool(pool).swap(recipient, false, -amount0Out.toInt256(), sqrtPriceLimitX96, abi.encode(msg.sender));
|
||||
}
|
||||
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes calldata data
|
||||
) external override {
|
||||
address sender = abi.decode(data, (address));
|
||||
|
||||
if (amount0Delta > 0) {
|
||||
IERC20(IUniswapV3Pool(msg.sender).token0()).transferFrom(sender, msg.sender, uint256(amount0Delta));
|
||||
} else {
|
||||
assert(amount1Delta > 0);
|
||||
IERC20(IUniswapV3Pool(msg.sender).token1()).transferFrom(sender, msg.sender, uint256(amount1Delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/swap-router-contracts/hardhat.config.ts
Normal file
70
lib/swap-router-contracts/hardhat.config.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import '@nomiclabs/hardhat-ethers'
|
||||
import '@nomiclabs/hardhat-etherscan'
|
||||
import '@nomiclabs/hardhat-waffle'
|
||||
import 'hardhat-typechain'
|
||||
import 'hardhat-watcher'
|
||||
import 'dotenv/config'
|
||||
|
||||
const DEFAULT_COMPILER_SETTINGS = {
|
||||
version: '0.7.6',
|
||||
settings: {
|
||||
evmVersion: 'istanbul',
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 1_000_000,
|
||||
},
|
||||
metadata: {
|
||||
bytecodeHash: 'none',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
networks: {
|
||||
hardhat: {
|
||||
allowUnlimitedContractSize: false,
|
||||
},
|
||||
mainnet: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
},
|
||||
ropsten: {
|
||||
url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
},
|
||||
rinkeby: {
|
||||
url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
},
|
||||
goerli: {
|
||||
url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
},
|
||||
kovan: {
|
||||
url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`,
|
||||
},
|
||||
arbitrumRinkeby: {
|
||||
url: `https://rinkeby.arbitrum.io/rpc`,
|
||||
},
|
||||
arbitrum: {
|
||||
url: `https://arb1.arbitrum.io/rpc`,
|
||||
},
|
||||
optimismKovan: {
|
||||
url: `https://kovan.optimism.io`,
|
||||
},
|
||||
optimism: {
|
||||
url: `https://mainnet.optimism.io`,
|
||||
},
|
||||
},
|
||||
etherscan: {
|
||||
// Your API key for Etherscan
|
||||
// Obtain one at https://etherscan.io/
|
||||
apiKey: process.env.ETHERSCAN_API_KEY,
|
||||
},
|
||||
solidity: {
|
||||
compilers: [DEFAULT_COMPILER_SETTINGS],
|
||||
},
|
||||
watcher: {
|
||||
test: {
|
||||
tasks: [{ command: 'test', params: { testFiles: ['{path}'] } }],
|
||||
files: ['./test/**/*'],
|
||||
verbose: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
68
lib/swap-router-contracts/package.json
Normal file
68
lib/swap-router-contracts/package.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "@uniswap/swap-router-contracts",
|
||||
"description": "Smart contracts for swapping on Uniswap V2 and V3",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "1.3.1",
|
||||
"homepage": "https://uniswap.org",
|
||||
"keywords": [
|
||||
"uniswap",
|
||||
"v2",
|
||||
"v3"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Uniswap/swap-router-contracts"
|
||||
},
|
||||
"files": [
|
||||
"contracts/base",
|
||||
"contracts/interfaces",
|
||||
"contracts/libraries",
|
||||
"artifacts/contracts/**/*.json",
|
||||
"!artifacts/contracts/**/*.dbg.json",
|
||||
"!artifacts/contracts/test/**/*",
|
||||
"!artifacts/contracts/base/**/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "3.4.2-solc-0.7",
|
||||
"@uniswap/v2-core": "^1.0.1",
|
||||
"@uniswap/v3-core": "^1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.4.4",
|
||||
"dotenv": "^14.2.0",
|
||||
"hardhat-watcher": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nomiclabs/hardhat-ethers": "^2.0.2",
|
||||
"@nomiclabs/hardhat-etherscan": "^2.1.8",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.1",
|
||||
"@typechain/ethers-v5": "^4.0.0",
|
||||
"@types/chai": "^4.2.6",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"chai": "^4.2.0",
|
||||
"decimal.js": "^10.2.1",
|
||||
"ethereum-waffle": "^3.0.2",
|
||||
"ethers": "^5.0.8",
|
||||
"hardhat": "^2.6.8",
|
||||
"hardhat-typechain": "^0.3.5",
|
||||
"is-svg": "^4.3.1",
|
||||
"mocha": "^6.2.2",
|
||||
"mocha-chai-jest-snapshot": "^1.1.0",
|
||||
"prettier": "^2.0.5",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.10",
|
||||
"solhint": "^3.2.1",
|
||||
"solhint-plugin-prettier": "^0.0.5",
|
||||
"ts-generator": "^0.1.1",
|
||||
"ts-node": "^8.5.4",
|
||||
"typechain": "^4.0.0",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "hardhat compile",
|
||||
"test": "hardhat test"
|
||||
}
|
||||
}
|
||||
388
lib/swap-router-contracts/test/ApproveAndCall.spec.ts
Normal file
388
lib/swap-router-contracts/test/ApproveAndCall.spec.ts
Normal file
@ -0,0 +1,388 @@
|
||||
import { defaultAbiCoder } from '@ethersproject/abi'
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { constants, Contract, ContractTransaction, Wallet } from 'ethers'
|
||||
import { solidityPack } from 'ethers/lib/utils'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { MockTimeSwapRouter02, TestERC20 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { ADDRESS_THIS, FeeAmount, TICK_SPACINGS } from './shared/constants'
|
||||
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
import { getMaxTick, getMinTick } from './shared/ticks'
|
||||
|
||||
enum ApprovalType {
|
||||
NOT_REQUIRED,
|
||||
MAX,
|
||||
MAX_MINUS_ONE,
|
||||
ZERO_THEN_MAX,
|
||||
ZERO_THEN_MAX_MINUS_ONE,
|
||||
}
|
||||
|
||||
describe('ApproveAndCall', function () {
|
||||
this.timeout(40000)
|
||||
let wallet: Wallet
|
||||
let trader: Wallet
|
||||
|
||||
const swapRouterFixture: Fixture<{
|
||||
factory: Contract
|
||||
router: MockTimeSwapRouter02
|
||||
nft: Contract
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
}> = async (wallets, provider) => {
|
||||
const { factory, router, tokens, nft } = await completeFixture(wallets, provider)
|
||||
|
||||
// approve & fund wallets
|
||||
for (const token of tokens) {
|
||||
await token.approve(nft.address, constants.MaxUint256)
|
||||
}
|
||||
|
||||
return {
|
||||
factory,
|
||||
router,
|
||||
tokens,
|
||||
nft,
|
||||
}
|
||||
}
|
||||
|
||||
let factory: Contract
|
||||
let router: MockTimeSwapRouter02
|
||||
let nft: Contract
|
||||
let tokens: [TestERC20, TestERC20, TestERC20]
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
function encodeSweepToken(token: string, amount: number) {
|
||||
const functionSignature = 'sweepToken(address,uint256)'
|
||||
return solidityPack(
|
||||
['bytes4', 'bytes'],
|
||||
[router.interface.getSighash(functionSignature), defaultAbiCoder.encode(['address', 'uint256'], [token, amount])]
|
||||
)
|
||||
}
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
;[wallet, trader] = await (ethers as any).getSigners()
|
||||
loadFixture = waffle.createFixtureLoader([wallet, trader])
|
||||
})
|
||||
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ factory, router, tokens, nft } = await loadFixture(swapRouterFixture))
|
||||
})
|
||||
|
||||
describe('swap and add', () => {
|
||||
async function createPool(tokenAddressA: string, tokenAddressB: string) {
|
||||
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
|
||||
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
|
||||
|
||||
await nft.createAndInitializePoolIfNecessary(
|
||||
tokenAddressA,
|
||||
tokenAddressB,
|
||||
FeeAmount.MEDIUM,
|
||||
encodePriceSqrt(1, 1)
|
||||
)
|
||||
|
||||
const liquidityParams = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 1000000,
|
||||
amount1Desired: 1000000,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
return nft.mint(liquidityParams)
|
||||
}
|
||||
|
||||
describe('approvals', () => {
|
||||
it('#approveMax', async () => {
|
||||
let approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123)
|
||||
expect(approvalType).to.be.eq(ApprovalType.MAX)
|
||||
|
||||
await router.approveMax(tokens[0].address)
|
||||
|
||||
approvalType = await router.callStatic.getApprovalType(tokens[0].address, 123)
|
||||
expect(approvalType).to.be.eq(ApprovalType.NOT_REQUIRED)
|
||||
})
|
||||
|
||||
it('#approveMax', async () => {
|
||||
await router.approveMax(tokens[0].address)
|
||||
})
|
||||
|
||||
it('#approveMaxMinusOne', async () => {
|
||||
await router.approveMaxMinusOne(tokens[0].address)
|
||||
})
|
||||
|
||||
describe('#approveZeroThenMax', async () => {
|
||||
it('from 0', async () => {
|
||||
await router.approveZeroThenMax(tokens[0].address)
|
||||
})
|
||||
it('from max', async () => {
|
||||
await router.approveMax(tokens[0].address)
|
||||
await router.approveZeroThenMax(tokens[0].address)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#approveZeroThenMax', async () => {
|
||||
it('from 0', async () => {
|
||||
await router.approveZeroThenMaxMinusOne(tokens[0].address)
|
||||
})
|
||||
it('from max', async () => {
|
||||
await router.approveMax(tokens[0].address)
|
||||
await router.approveZeroThenMaxMinusOne(tokens[0].address)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('#mint and #increaseLiquidity', async () => {
|
||||
await createPool(tokens[0].address, tokens[1].address)
|
||||
const pool = await factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM)
|
||||
|
||||
// approve in advance
|
||||
await router.approveMax(tokens[0].address)
|
||||
await router.approveMax(tokens[1].address)
|
||||
|
||||
// send dummy amount of tokens to the pair in advance
|
||||
const amount = 1000
|
||||
await tokens[0].transfer(router.address, amount)
|
||||
await tokens[1].transfer(router.address, amount)
|
||||
expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(amount)
|
||||
expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(amount)
|
||||
|
||||
let poolBalance0Before = await tokens[0].balanceOf(pool)
|
||||
let poolBalance1Before = await tokens[1].balanceOf(pool)
|
||||
|
||||
// perform the mint
|
||||
await router.mint({
|
||||
token0: tokens[0].address,
|
||||
token1: tokens[1].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: trader.address,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
})
|
||||
|
||||
expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0)
|
||||
expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0)
|
||||
expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount)
|
||||
expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount)
|
||||
|
||||
expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1)
|
||||
|
||||
// send more tokens
|
||||
await tokens[0].transfer(router.address, amount)
|
||||
await tokens[1].transfer(router.address, amount)
|
||||
|
||||
// perform the increaseLiquidity
|
||||
await router.increaseLiquidity({
|
||||
token0: tokens[0].address,
|
||||
token1: tokens[1].address,
|
||||
tokenId: 2,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
})
|
||||
|
||||
expect((await tokens[0].balanceOf(router.address)).toNumber()).to.be.eq(0)
|
||||
expect((await tokens[1].balanceOf(router.address)).toNumber()).to.be.eq(0)
|
||||
expect((await tokens[0].balanceOf(pool)).toNumber()).to.be.eq(poolBalance0Before.toNumber() + amount * 2)
|
||||
expect((await tokens[1].balanceOf(pool)).toNumber()).to.be.eq(poolBalance1Before.toNumber() + amount * 2)
|
||||
|
||||
expect((await nft.balanceOf(trader.address)).toNumber()).to.be.eq(1)
|
||||
})
|
||||
|
||||
describe('single-asset add', () => {
|
||||
beforeEach('create 0-1 pool', async () => {
|
||||
await createPool(tokens[0].address, tokens[1].address)
|
||||
})
|
||||
|
||||
async function singleAssetAddExactInput(
|
||||
tokenIn: string,
|
||||
tokenOut: string,
|
||||
amountIn: number,
|
||||
amountOutMinimum: number
|
||||
): Promise<ContractTransaction> {
|
||||
// encode the exact input swap
|
||||
const params = {
|
||||
path: encodePath([tokenIn, tokenOut], [FeeAmount.MEDIUM]),
|
||||
recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller
|
||||
amountIn,
|
||||
amountOutMinimum,
|
||||
}
|
||||
// ensure that the swap fails if the limit is any tighter
|
||||
const amountOut = await router.connect(trader).callStatic.exactInput(params)
|
||||
expect(amountOut.toNumber()).to.be.eq(amountOutMinimum)
|
||||
const data = [router.interface.encodeFunctionData('exactInput', [params])]
|
||||
|
||||
// encode the pull (we take the same as the amountOutMinimum, assuming a 50/50 range)
|
||||
data.push(router.interface.encodeFunctionData('pull', [tokenIn, amountOutMinimum]))
|
||||
|
||||
// encode the approves
|
||||
data.push(router.interface.encodeFunctionData('approveMax', [tokenIn]))
|
||||
data.push(router.interface.encodeFunctionData('approveMax', [tokenOut]))
|
||||
|
||||
// encode the add liquidity
|
||||
const [token0, token1] =
|
||||
tokenIn.toLowerCase() < tokenOut.toLowerCase() ? [tokenIn, tokenOut] : [tokenOut, tokenIn]
|
||||
const liquidityParams = {
|
||||
token0,
|
||||
token1,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: trader.address,
|
||||
amount0Desired: amountOutMinimum,
|
||||
amount1Desired: amountOutMinimum,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
data.push(
|
||||
router.interface.encodeFunctionData('callPositionManager', [
|
||||
nft.interface.encodeFunctionData('mint', [liquidityParams]),
|
||||
])
|
||||
)
|
||||
|
||||
// encode the sweeps
|
||||
data.push(encodeSweepToken(tokenIn, 0))
|
||||
data.push(encodeSweepToken(tokenOut, 0))
|
||||
|
||||
return router.connect(trader)['multicall(bytes[])'](data)
|
||||
}
|
||||
|
||||
it('0 -> 1', async () => {
|
||||
const amountIn = 1000
|
||||
const amountOutMinimum = 996
|
||||
|
||||
// prep for the swap + add by sending tokens
|
||||
await tokens[0].transfer(trader.address, amountIn + amountOutMinimum)
|
||||
await tokens[0].connect(trader).approve(router.address, amountIn + amountOutMinimum)
|
||||
|
||||
const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address)
|
||||
const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address)
|
||||
expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn + amountOutMinimum)
|
||||
expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0)
|
||||
|
||||
const traderNFTBalanceBefore = await nft.balanceOf(trader.address)
|
||||
expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0)
|
||||
|
||||
await singleAssetAddExactInput(tokens[0].address, tokens[1].address, amountIn, amountOutMinimum)
|
||||
|
||||
const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address)
|
||||
const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address)
|
||||
expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0)
|
||||
expect(traderToken1BalanceAfter.toNumber()).to.be.eq(1) // dust
|
||||
|
||||
const traderNFTBalanceAfter = await nft.balanceOf(trader.address)
|
||||
expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('any-asset add', () => {
|
||||
beforeEach('create 0-1, 0-2, and 1-2 pools pools', async () => {
|
||||
await createPool(tokens[0].address, tokens[1].address)
|
||||
await createPool(tokens[0].address, tokens[2].address)
|
||||
await createPool(tokens[1].address, tokens[2].address)
|
||||
})
|
||||
|
||||
async function anyAssetAddExactInput(
|
||||
tokenStart: string,
|
||||
tokenA: string,
|
||||
tokenB: string,
|
||||
amountIn: number,
|
||||
amountOutMinimum: number
|
||||
): Promise<ContractTransaction> {
|
||||
// encode the exact input swaps
|
||||
let params = {
|
||||
path: encodePath([tokenStart, tokenA], [FeeAmount.MEDIUM]),
|
||||
recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller
|
||||
amountIn,
|
||||
amountOutMinimum,
|
||||
}
|
||||
// ensure that the swap fails if the limit is any tighter
|
||||
let amountOut = await router.connect(trader).callStatic.exactInput(params)
|
||||
expect(amountOut.toNumber()).to.be.eq(amountOutMinimum)
|
||||
let data = [router.interface.encodeFunctionData('exactInput', [params])]
|
||||
|
||||
// encode the exact input swaps
|
||||
params = {
|
||||
path: encodePath([tokenStart, tokenB], [FeeAmount.MEDIUM]),
|
||||
recipient: ADDRESS_THIS, // have to send to the router, as it will be adding liquidity for the caller
|
||||
amountIn,
|
||||
amountOutMinimum,
|
||||
}
|
||||
// ensure that the swap fails if the limit is any tighter
|
||||
amountOut = await router.connect(trader).callStatic.exactInput(params)
|
||||
expect(amountOut.toNumber()).to.be.eq(amountOutMinimum)
|
||||
data.push(router.interface.encodeFunctionData('exactInput', [params]))
|
||||
|
||||
// encode the approves
|
||||
data.push(router.interface.encodeFunctionData('approveMax', [tokenA]))
|
||||
data.push(router.interface.encodeFunctionData('approveMax', [tokenB]))
|
||||
|
||||
// encode the add liquidity
|
||||
const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
const liquidityParams = {
|
||||
token0,
|
||||
token1,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: trader.address,
|
||||
amount0Desired: amountOutMinimum,
|
||||
amount1Desired: amountOutMinimum,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
data.push(
|
||||
router.interface.encodeFunctionData('callPositionManager', [
|
||||
nft.interface.encodeFunctionData('mint', [liquidityParams]),
|
||||
])
|
||||
)
|
||||
|
||||
// encode the sweeps
|
||||
data.push(encodeSweepToken(tokenA, 0))
|
||||
data.push(encodeSweepToken(tokenB, 0))
|
||||
|
||||
return router.connect(trader)['multicall(bytes[])'](data)
|
||||
}
|
||||
|
||||
it('0 -> 1 and 0 -> 2', async () => {
|
||||
const amountIn = 1000
|
||||
const amountOutMinimum = 996
|
||||
|
||||
// prep for the swap + add by sending tokens
|
||||
await tokens[0].transfer(trader.address, amountIn * 2)
|
||||
await tokens[0].connect(trader).approve(router.address, amountIn * 2)
|
||||
|
||||
const traderToken0BalanceBefore = await tokens[0].balanceOf(trader.address)
|
||||
const traderToken1BalanceBefore = await tokens[1].balanceOf(trader.address)
|
||||
const traderToken2BalanceBefore = await tokens[2].balanceOf(trader.address)
|
||||
expect(traderToken0BalanceBefore.toNumber()).to.be.eq(amountIn * 2)
|
||||
expect(traderToken1BalanceBefore.toNumber()).to.be.eq(0)
|
||||
expect(traderToken2BalanceBefore.toNumber()).to.be.eq(0)
|
||||
|
||||
const traderNFTBalanceBefore = await nft.balanceOf(trader.address)
|
||||
expect(traderNFTBalanceBefore.toNumber()).to.be.eq(0)
|
||||
|
||||
await anyAssetAddExactInput(tokens[0].address, tokens[1].address, tokens[2].address, amountIn, amountOutMinimum)
|
||||
|
||||
const traderToken0BalanceAfter = await tokens[0].balanceOf(trader.address)
|
||||
const traderToken1BalanceAfter = await tokens[1].balanceOf(trader.address)
|
||||
const traderToken2BalanceAfter = await tokens[2].balanceOf(trader.address)
|
||||
expect(traderToken0BalanceAfter.toNumber()).to.be.eq(0)
|
||||
expect(traderToken1BalanceAfter.toNumber()).to.be.eq(0)
|
||||
expect(traderToken2BalanceAfter.toNumber()).to.be.eq(0)
|
||||
|
||||
const traderNFTBalanceAfter = await nft.balanceOf(trader.address)
|
||||
expect(traderNFTBalanceAfter.toNumber()).to.be.eq(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
58
lib/swap-router-contracts/test/ImmutableState.spec.ts
Normal file
58
lib/swap-router-contracts/test/ImmutableState.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Contract } from 'ethers'
|
||||
import { waffle, ethers } from 'hardhat'
|
||||
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { ImmutableStateTest } from '../typechain'
|
||||
import { expect } from './shared/expect'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { v2FactoryFixture } from './shared/externalFixtures'
|
||||
|
||||
describe('ImmutableState', () => {
|
||||
const fixture: Fixture<{
|
||||
factoryV2: Contract
|
||||
nft: Contract
|
||||
state: ImmutableStateTest
|
||||
}> = async (wallets, provider) => {
|
||||
const { factory: factoryV2 } = await v2FactoryFixture(wallets, provider)
|
||||
const { nft } = await completeFixture(wallets, provider)
|
||||
|
||||
const stateFactory = await ethers.getContractFactory('ImmutableStateTest')
|
||||
const state = (await stateFactory.deploy(factoryV2.address, nft.address)) as ImmutableStateTest
|
||||
|
||||
return {
|
||||
nft,
|
||||
factoryV2,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
let factoryV2: Contract
|
||||
let nft: Contract
|
||||
let state: ImmutableStateTest
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
loadFixture = waffle.createFixtureLoader(await (ethers as any).getSigners())
|
||||
})
|
||||
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ factoryV2, nft, state } = await loadFixture(fixture))
|
||||
})
|
||||
|
||||
it('bytecode size', async () => {
|
||||
expect(((await state.provider.getCode(state.address)).length - 2) / 2).to.matchSnapshot()
|
||||
})
|
||||
|
||||
describe('#factoryV2', () => {
|
||||
it('points to v2 core factory', async () => {
|
||||
expect(await state.factoryV2()).to.eq(factoryV2.address)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#positionManager', () => {
|
||||
it('points to NFT', async () => {
|
||||
expect(await state.positionManager()).to.eq(nft.address)
|
||||
})
|
||||
})
|
||||
})
|
||||
184
lib/swap-router-contracts/test/MixedRouteQuoterV1.integ.ts
Normal file
184
lib/swap-router-contracts/test/MixedRouteQuoterV1.integ.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { expect } from 'chai'
|
||||
import { BigNumber } from 'ethers'
|
||||
import { MixedRouteQuoterV1 } from '../typechain'
|
||||
|
||||
import hre, { ethers } from 'hardhat'
|
||||
import { encodePath } from './shared/path'
|
||||
import { expandTo18Decimals, expandToNDecimals } from './shared/expandTo18Decimals'
|
||||
import { FeeAmount, V2_FEE_PLACEHOLDER } from './shared/constants'
|
||||
|
||||
const V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984'
|
||||
const V2_FACTORY = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'
|
||||
|
||||
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
|
||||
const USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
|
||||
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
||||
const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'
|
||||
const DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
||||
|
||||
/// @dev basic V2 routes
|
||||
const DAI_V2_UNI_V2_WETH = encodePath([DAI, UNI, WETH], [V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER])
|
||||
const USDC_V2_UNI_V2_WETH = encodePath([USDC, UNI, WETH], [V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER])
|
||||
|
||||
/// @dev basic V3 routes
|
||||
const USDC_V3_USDT = encodePath([USDC, USDT], [FeeAmount.LOW])
|
||||
const UNI_V3_WETH = encodePath([UNI, WETH], [FeeAmount.MEDIUM])
|
||||
|
||||
/// @dev stablecoin IL routes
|
||||
const USDT_V3_DAI_V2_USDC = encodePath([USDT, DAI, USDC], [FeeAmount.LOW, V2_FEE_PLACEHOLDER])
|
||||
const DAI_V3_USDC_V2_USDT = encodePath([DAI, USDC, USDT], [100, V2_FEE_PLACEHOLDER])
|
||||
|
||||
/// @dev erc20 IL routes
|
||||
// V3 - V2
|
||||
const UNI_V3_WETH_V2_DAI = encodePath([UNI, WETH, DAI], [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER])
|
||||
const USDC_V3_UNI_V2_WETH = encodePath([USDC, UNI, WETH], [FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER])
|
||||
// V2 - V3
|
||||
const UNI_V2_WETH_V3_DAI = encodePath([UNI, WETH, DAI], [V2_FEE_PLACEHOLDER, FeeAmount.MEDIUM])
|
||||
|
||||
/// @dev complex IL routes
|
||||
// (use two V3 pools)
|
||||
const DAI_V3_3000_UNI_V2_USDT_V3_3000_WETH = encodePath(
|
||||
[DAI, UNI, USDT, WETH],
|
||||
[FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER, FeeAmount.MEDIUM]
|
||||
)
|
||||
// (use two V2 pools)
|
||||
const DAI_V3_3000_UNI_V2_USDT_V2_WETH = encodePath(
|
||||
[DAI, UNI, USDT, WETH],
|
||||
[FeeAmount.MEDIUM, V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER]
|
||||
)
|
||||
|
||||
describe('MixedRouteQuoterV1 integration tests', function () {
|
||||
let mixedRouteQuoter: MixedRouteQuoterV1
|
||||
|
||||
this.timeout(100000)
|
||||
|
||||
before(async function () {
|
||||
if (!process.env.ARCHIVE_RPC_URL) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
await hre.network.provider.request({
|
||||
method: 'hardhat_reset',
|
||||
params: [
|
||||
{
|
||||
forking: {
|
||||
jsonRpcUrl: process.env.ARCHIVE_RPC_URL,
|
||||
blockNumber: 14390000,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const MixedRouteQuoterV1Factory = await ethers.getContractFactory('MixedRouteQuoterV1')
|
||||
mixedRouteQuoter = (await MixedRouteQuoterV1Factory.deploy(V3_FACTORY, V2_FACTORY, WETH)) as MixedRouteQuoterV1
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Disable mainnet forking to avoid effecting other tests.
|
||||
await hre.network.provider.request({
|
||||
method: 'hardhat_reset',
|
||||
params: [],
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test values only valid starting at block 14390000
|
||||
*/
|
||||
it('sets block number correctly', async () => {
|
||||
const blockNumber = BigNumber.from(
|
||||
await hre.network.provider.request({
|
||||
method: 'eth_blockNumber',
|
||||
params: [],
|
||||
})
|
||||
)
|
||||
/// @dev +1 so 14390001 since we just requested
|
||||
expect(blockNumber.eq(14390001)).to.be.true
|
||||
})
|
||||
|
||||
describe('quotes stablecoin only paths correctly', () => {
|
||||
/// @dev the amount must be expanded to the decimals of the first token in the path
|
||||
it('V3-V2 stablecoin path with 6 decimal in start of path', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](USDT_V3_DAI_V2_USDC, expandToNDecimals(10000, 6))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('9966336832'))
|
||||
expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x10c6727487c45717095f'))).to.be.true
|
||||
})
|
||||
|
||||
it('V3-V2 stablecoin path with 6 decimal in middle of path', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](DAI_V3_USDC_V2_USDT, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('9959354898'))
|
||||
expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x10c715093f77e3073634'))).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('V2-V2 quotes', () => {
|
||||
it('quotes V2-V2 correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](DAI_V2_UNI_V2_WETH, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('2035189623576328665'))
|
||||
expect(v3SqrtPriceX96AfterList.every((el) => el.eq(0))).to.be.true
|
||||
expect(v3InitializedTicksCrossedList.every((el) => el == 0)).to.be.true
|
||||
})
|
||||
|
||||
it('quotes V2 (6 decimal stablecoin) -V2 correctly', async () => {
|
||||
const { amountOut } = await mixedRouteQuoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
USDC_V2_UNI_V2_WETH,
|
||||
expandToNDecimals(10000, 6)
|
||||
)
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('1989381322826753150'))
|
||||
})
|
||||
})
|
||||
|
||||
it('quotes V3-V2 erc20s with mixed decimal scales correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](USDC_V3_UNI_V2_WETH, expandToNDecimals(10000, 6))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('3801923847986895918')) // 3.801923847986895918
|
||||
expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x3110863ba621ac3915fd'))).to.be.true
|
||||
})
|
||||
|
||||
it('quotes V3-V2 correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](UNI_V3_WETH_V2_DAI, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('80675538331724434694636'))
|
||||
expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0x0e83f285cb58c4cca14fb78b'))).to.be.true
|
||||
})
|
||||
|
||||
it('quotes V3-V2-V3 correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](DAI_V3_3000_UNI_V2_USDT_V3_3000_WETH, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('886596560223108447'))
|
||||
expect(v3SqrtPriceX96AfterList[0].eq(BigNumber.from('0xfffd8963efd1fc6a506488495d951d5263988d25'))).to.be.true
|
||||
expect(v3SqrtPriceX96AfterList[2].eq(BigNumber.from('0x034b624fce51aba62a4722'))).to.be.true
|
||||
})
|
||||
|
||||
it('quotes V2-V3 correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](UNI_V2_WETH_V3_DAI, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut).eq(BigNumber.from('81108655328627859394525'))
|
||||
expect(v3SqrtPriceX96AfterList[1].eq(BigNumber.from('0x0518b75d40eb50192903493d'))).to.be.true
|
||||
})
|
||||
|
||||
it('quotes only V3 correctly', async () => {
|
||||
const { amountOut, v3SqrtPriceX96AfterList, v3InitializedTicksCrossedList } = await mixedRouteQuoter.callStatic[
|
||||
'quoteExactInput(bytes,uint256)'
|
||||
](UNI_V3_WETH, expandTo18Decimals(10000))
|
||||
|
||||
expect(amountOut.eq(BigNumber.from('32215526370828998898'))).to.be.true
|
||||
})
|
||||
})
|
||||
460
lib/swap-router-contracts/test/MixedRouteQuoterV1.spec.ts
Normal file
460
lib/swap-router-contracts/test/MixedRouteQuoterV1.spec.ts
Normal file
@ -0,0 +1,460 @@
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { constants, Wallet, Contract, BigNumber } from 'ethers'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { MixedRouteQuoterV1, TestERC20 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { FeeAmount, V2_FEE_PLACEHOLDER } from './shared/constants'
|
||||
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
||||
import { expandTo18Decimals } from './shared/expandTo18Decimals'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
import {
|
||||
createPair,
|
||||
createPool,
|
||||
createPoolWithMultiplePositions,
|
||||
createPoolWithZeroTickInitialized,
|
||||
} from './shared/quoter'
|
||||
import snapshotGasCost from './shared/snapshotGasCost'
|
||||
|
||||
import { abi as PAIR_V2_ABI } from '@uniswap/v2-core/build/UniswapV2Pair.json'
|
||||
|
||||
const V3_MAX_FEE = 999999 // = 1_000_000 - 1 since must be < 1_000_000
|
||||
|
||||
describe('MixedRouteQuoterV1', function () {
|
||||
this.timeout(40000)
|
||||
let wallet: Wallet
|
||||
let trader: Wallet
|
||||
|
||||
const swapRouterFixture: Fixture<{
|
||||
nft: Contract
|
||||
factoryV2: Contract
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
quoter: MixedRouteQuoterV1
|
||||
}> = async (wallets, provider) => {
|
||||
const { weth9, factory, factoryV2, router, tokens, nft } = await completeFixture(wallets, provider)
|
||||
|
||||
// approve & fund wallets
|
||||
for (const token of tokens) {
|
||||
await token.approve(router.address, constants.MaxUint256)
|
||||
await token.approve(nft.address, constants.MaxUint256)
|
||||
await token.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
await token.transfer(trader.address, expandTo18Decimals(1_000_000))
|
||||
}
|
||||
|
||||
const quoterFactory = await ethers.getContractFactory('MixedRouteQuoterV1')
|
||||
quoter = (await quoterFactory.deploy(factory.address, factoryV2.address, weth9.address)) as MixedRouteQuoterV1
|
||||
|
||||
return {
|
||||
tokens,
|
||||
nft,
|
||||
factoryV2,
|
||||
quoter,
|
||||
}
|
||||
}
|
||||
|
||||
let nft: Contract
|
||||
let factoryV2: Contract
|
||||
let tokens: [TestERC20, TestERC20, TestERC20]
|
||||
let quoter: MixedRouteQuoterV1
|
||||
|
||||
let pair01Address, pair02Address, pair12Address: string
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
const wallets = await (ethers as any).getSigners()
|
||||
;[wallet, trader] = wallets
|
||||
loadFixture = waffle.createFixtureLoader(wallets)
|
||||
})
|
||||
|
||||
// helper for getting weth and token balances
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ tokens, nft, factoryV2, quoter } = await loadFixture(swapRouterFixture))
|
||||
})
|
||||
|
||||
const addLiquidityV2 = async (
|
||||
pairAddress: string,
|
||||
token0: TestERC20,
|
||||
token1: TestERC20,
|
||||
amount0: string,
|
||||
amount1: string
|
||||
) => {
|
||||
const pair = new Contract(pairAddress, PAIR_V2_ABI, wallet)
|
||||
expect(await pair.callStatic.token0()).to.equal(token0.address)
|
||||
expect(await pair.callStatic.token1()).to.equal(token1.address)
|
||||
// seed the pairs with liquidity
|
||||
|
||||
const [reserve0Before, reserve1Before]: [BigNumber, BigNumber] = await pair.callStatic.getReserves()
|
||||
|
||||
const token0BalanceBefore = await token0.balanceOf(pairAddress)
|
||||
const token1BalanceBefore = await token1.balanceOf(pairAddress)
|
||||
|
||||
await token0.transfer(pairAddress, ethers.utils.parseEther(amount0))
|
||||
await token1.transfer(pairAddress, ethers.utils.parseEther(amount1))
|
||||
|
||||
expect(await token0.balanceOf(pairAddress)).to.equal(token0BalanceBefore.add(ethers.utils.parseEther(amount0)))
|
||||
expect(await token1.balanceOf(pairAddress)).to.equal(token1BalanceBefore.add(ethers.utils.parseEther(amount1)))
|
||||
|
||||
await pair.mint(wallet.address) // update the reserves
|
||||
|
||||
const [reserve0, reserve1] = await pair.callStatic.getReserves()
|
||||
expect(reserve0).to.equal(reserve0Before.add(ethers.utils.parseEther(amount0)))
|
||||
expect(reserve1).to.equal(reserve1Before.add(ethers.utils.parseEther(amount1)))
|
||||
}
|
||||
|
||||
describe('quotes', () => {
|
||||
beforeEach(async () => {
|
||||
await createPool(nft, wallet, tokens[0].address, tokens[1].address)
|
||||
await createPool(nft, wallet, tokens[1].address, tokens[2].address)
|
||||
await createPoolWithMultiplePositions(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
/// @dev Create V2 Pairs
|
||||
pair01Address = await createPair(factoryV2, tokens[0].address, tokens[1].address)
|
||||
pair12Address = await createPair(factoryV2, tokens[1].address, tokens[2].address)
|
||||
pair02Address = await createPair(factoryV2, tokens[0].address, tokens[2].address)
|
||||
|
||||
await addLiquidityV2(pair01Address, tokens[0], tokens[1], '1000000', '1000000')
|
||||
await addLiquidityV2(pair12Address, tokens[1], tokens[2], '1000000', '1000000')
|
||||
await addLiquidityV2(pair02Address, tokens[0], tokens[2], '1000000', '1000000')
|
||||
})
|
||||
|
||||
/// @dev Test running the old suite on the new function but with protocolFlags only being V3[]
|
||||
describe('#quoteExactInput V3 only', () => {
|
||||
it('0 -> 2 cross 2 tick', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(amountOut).to.eq(9871)
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 2 tick where after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is -120.
|
||||
// -120 is an initialized tick for this pool. We check that we don't count it.
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
6200
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('78757224507315167622282810783')
|
||||
expect(v3InitializedTicksCrossedList.length).to.eq(1)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(amountOut).to.eq(6143)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 1 tick', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
4000
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('78926452400586371254602774705')
|
||||
expect(amountOut).to.eq(3971)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick, starting tick not initialized', async () => {
|
||||
// Tick before 0, tick after -1.
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('79227483487511329217250071027')
|
||||
expect(amountOut).to.eq(8)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick, starting tick initialized', async () => {
|
||||
// Tick before 0, tick after -1. Tick 0 initialized.
|
||||
await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('79227817515327498931091950511')
|
||||
expect(amountOut).to.eq(8)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('80001962924147897865541384515')
|
||||
expect(v3InitializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(9871)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2 where tick after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is 120.
|
||||
// 120 is an initialized tick for this pool. We check we don't count it.
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
6250
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('79705728824507063507279123685')
|
||||
expect(v3InitializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(6190)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 0 tick, starting tick initialized', async () => {
|
||||
// Tick 0 initialized. Tick after = 1
|
||||
await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
200
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('79235729830182478001034429156')
|
||||
expect(v3InitializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(198)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 0 tick, starting tick not initialized', async () => {
|
||||
// Tick 0 initialized. Tick after = 1
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
103
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('79235858216754624215638319723')
|
||||
expect(v3InitializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(101)
|
||||
})
|
||||
|
||||
it('2 -> 1', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('80018067294531553039351583520')
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(amountOut).to.eq(9871)
|
||||
})
|
||||
|
||||
it('0 -> 2 -> 1', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
v3SqrtPriceX96AfterList,
|
||||
v3InitializedTicksCrossedList,
|
||||
v3SwapGasEstimate,
|
||||
} = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM, FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(v3SwapGasEstimate)
|
||||
expect(v3SqrtPriceX96AfterList.length).to.eq(2)
|
||||
expect(v3SqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
|
||||
expect(v3SqrtPriceX96AfterList[1]).to.eq('80007846861567212939802016351')
|
||||
expect(v3InitializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(v3InitializedTicksCrossedList[1]).to.eq(0)
|
||||
expect(amountOut).to.eq(9745)
|
||||
})
|
||||
})
|
||||
|
||||
/// @dev Test running the old suite on the new function but with protocolFlags only being V2[]
|
||||
describe('#quoteExactInput V2 only', () => {
|
||||
it('0 -> 2', async () => {
|
||||
const { amountOut, v3SwapGasEstimate } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[2].address], [V2_FEE_PLACEHOLDER]),
|
||||
10000
|
||||
)
|
||||
|
||||
expect(amountOut).to.eq(9969)
|
||||
})
|
||||
|
||||
it('0 -> 1 -> 2', async () => {
|
||||
const { amountOut, v3SwapGasEstimate } = await quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath(
|
||||
[tokens[0].address, tokens[1].address, tokens[2].address],
|
||||
[V2_FEE_PLACEHOLDER, V2_FEE_PLACEHOLDER]
|
||||
),
|
||||
10000
|
||||
)
|
||||
|
||||
expect(amountOut).to.eq(9939)
|
||||
})
|
||||
})
|
||||
|
||||
/// @dev Test copied over from QuoterV2.spec.ts
|
||||
describe('#quoteExactInputSingle V3', () => {
|
||||
it('0 -> 2', async () => {
|
||||
const {
|
||||
amountOut: quote,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInputSingleV3({
|
||||
tokenIn: tokens[0].address,
|
||||
tokenOut: tokens[2].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amountIn: 10000,
|
||||
// -2%
|
||||
sqrtPriceLimitX96: encodePriceSqrt(100, 102),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossed).to.eq(2)
|
||||
expect(quote).to.eq(9871)
|
||||
expect(sqrtPriceX96After).to.eq('78461846509168490764501028180')
|
||||
})
|
||||
|
||||
it('2 -> 0', async () => {
|
||||
const {
|
||||
amountOut: quote,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInputSingleV3({
|
||||
tokenIn: tokens[2].address,
|
||||
tokenOut: tokens[0].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amountIn: 10000,
|
||||
// +2%
|
||||
sqrtPriceLimitX96: encodePriceSqrt(102, 100),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossed).to.eq(2)
|
||||
expect(quote).to.eq(9871)
|
||||
expect(sqrtPriceX96After).to.eq('80001962924147897865541384515')
|
||||
})
|
||||
})
|
||||
|
||||
/// @dev Test the new function for fetching a single V2 pair quote on chain (exactIn)
|
||||
describe('#quoteExactInputSingleV2', () => {
|
||||
it('0 -> 2', async () => {
|
||||
const amountIn = 10000
|
||||
const tokenIn = tokens[0].address
|
||||
const tokenOut = tokens[2].address
|
||||
const quote = await quoter.callStatic.quoteExactInputSingleV2({ tokenIn, tokenOut, amountIn })
|
||||
|
||||
expect(quote).to.eq(9969)
|
||||
})
|
||||
|
||||
it('2 -> 0', async () => {
|
||||
const amountIn = 10000
|
||||
const tokenIn = tokens[2].address
|
||||
const tokenOut = tokens[0].address
|
||||
const quote = await quoter.callStatic.quoteExactInputSingleV2({ tokenIn, tokenOut, amountIn })
|
||||
|
||||
expect(quote).to.eq(9969)
|
||||
})
|
||||
|
||||
describe('+ with imbalanced pairs', () => {
|
||||
before(async () => {
|
||||
await addLiquidityV2(pair12Address, tokens[1], tokens[2], '1000000', '1000')
|
||||
})
|
||||
|
||||
it('1 -> 2', async () => {
|
||||
const amountIn = 2_000_000
|
||||
const tokenIn = tokens[1].address
|
||||
const tokenOut = tokens[2].address
|
||||
const quote = await quoter.callStatic.quoteExactInputSingleV2({ tokenIn, tokenOut, amountIn })
|
||||
|
||||
expect(quote).to.eq(1993999)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing bit masking for protocol selection', () => {
|
||||
it('when given the max v3 fee, should still route v3 and revert because pool DNE', async () => {
|
||||
/// @define 999999 is the max fee that can be set on a V3 pool per the factory
|
||||
/// in this environment this pool does not exist, and thus the call should revert
|
||||
/// - however, if the bitmask fails to catch this the call will succeed and route to V2
|
||||
/// - thus, we expect it to be reverted.
|
||||
await expect(
|
||||
quoter.callStatic['quoteExactInput(bytes,uint256)'](
|
||||
encodePath([tokens[0].address, tokens[1].address], [V3_MAX_FEE]),
|
||||
10000
|
||||
)
|
||||
).to.be.reverted
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
50
lib/swap-router-contracts/test/MulticallExtended.spec.ts
Normal file
50
lib/swap-router-contracts/test/MulticallExtended.spec.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { constants } from 'ethers'
|
||||
import { ethers } from 'hardhat'
|
||||
import { TestMulticallExtended } from '../typechain/TestMulticallExtended'
|
||||
import { expect } from './shared/expect'
|
||||
|
||||
describe('MulticallExtended', async () => {
|
||||
let multicall: TestMulticallExtended
|
||||
|
||||
beforeEach('create multicall', async () => {
|
||||
const multicallTestFactory = await ethers.getContractFactory('TestMulticallExtended')
|
||||
multicall = (await multicallTestFactory.deploy()) as TestMulticallExtended
|
||||
})
|
||||
|
||||
it('fails deadline check', async () => {
|
||||
await multicall.setTime(1)
|
||||
await expect(
|
||||
multicall['multicall(uint256,bytes[])'](0, [
|
||||
multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']),
|
||||
])
|
||||
).to.be.revertedWith('Transaction too old')
|
||||
})
|
||||
|
||||
it('passes deadline check', async () => {
|
||||
const [data] = await multicall.callStatic['multicall(uint256,bytes[])'](0, [
|
||||
multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']),
|
||||
])
|
||||
const {
|
||||
tuple: { a, b },
|
||||
} = multicall.interface.decodeFunctionResult('functionThatReturnsTuple', data)
|
||||
expect(b).to.eq(1)
|
||||
expect(a).to.eq(2)
|
||||
})
|
||||
|
||||
it('fails previousBlockhash check', async () => {
|
||||
await expect(
|
||||
multicall['multicall(bytes32,bytes[])'](constants.HashZero, [
|
||||
multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']),
|
||||
])
|
||||
).to.be.revertedWith('Blockhash')
|
||||
})
|
||||
|
||||
it('passes previousBlockhash check', async () => {
|
||||
const block = await ethers.provider.getBlock('latest')
|
||||
await expect(
|
||||
multicall['multicall(bytes32,bytes[])'](block.hash, [
|
||||
multicall.interface.encodeFunctionData('functionThatReturnsTuple', ['1', '2']),
|
||||
])
|
||||
).to.not.be.reverted
|
||||
})
|
||||
})
|
||||
447
lib/swap-router-contracts/test/OracleSlippage.spec.ts
Normal file
447
lib/swap-router-contracts/test/OracleSlippage.spec.ts
Normal file
@ -0,0 +1,447 @@
|
||||
import { constants, ContractFactory } from 'ethers'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { MockObservations, OracleSlippageTest } from '../typechain'
|
||||
import { FeeAmount } from './shared/constants'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
|
||||
const tokens = [
|
||||
'0x0000000000000000000000000000000000000001',
|
||||
'0x0000000000000000000000000000000000000002',
|
||||
'0x0000000000000000000000000000000000000003',
|
||||
]
|
||||
|
||||
describe('OracleSlippage', function () {
|
||||
this.timeout(40000)
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
let oracle: OracleSlippageTest
|
||||
let mockObservationsFactory: ContractFactory
|
||||
|
||||
const oracleTestFixture = async () => {
|
||||
const oracleFactory = await ethers.getContractFactory('OracleSlippageTest')
|
||||
const oracle = await oracleFactory.deploy(constants.AddressZero, constants.AddressZero)
|
||||
|
||||
return oracle as OracleSlippageTest
|
||||
}
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
loadFixture = waffle.createFixtureLoader(await (ethers as any).getSigners())
|
||||
})
|
||||
|
||||
beforeEach('deploy fixture', async () => {
|
||||
oracle = await loadFixture(oracleTestFixture)
|
||||
})
|
||||
|
||||
before('create mockObservationsFactory', async () => {
|
||||
mockObservationsFactory = await ethers.getContractFactory('MockObservations')
|
||||
})
|
||||
|
||||
async function createMockPool(
|
||||
tokenA: string,
|
||||
tokenB: string,
|
||||
fee: FeeAmount,
|
||||
blockTimestamps: number[],
|
||||
ticks: number[],
|
||||
mockLowObservationCardinality = false
|
||||
): Promise<MockObservations> {
|
||||
const mockPool = await mockObservationsFactory.deploy(blockTimestamps, ticks, mockLowObservationCardinality)
|
||||
await oracle.registerPool(mockPool.address, tokenA, tokenB, fee)
|
||||
await oracle.setTime(blockTimestamps[blockTimestamps.length - 1])
|
||||
return mockPool as MockObservations
|
||||
}
|
||||
|
||||
describe('#getBlockStartingAndCurrentTick', () => {
|
||||
it('fails when observationCardinality == 1', async () => {
|
||||
const mockPool = await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 0, 0], true)
|
||||
await expect(oracle.testGetBlockStartingAndCurrentTick(mockPool.address)).to.be.revertedWith('NEO')
|
||||
})
|
||||
|
||||
it('works when ticks are the same in the same block', async () => {
|
||||
const mockPool = await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
const { blockStartingTick, currentTick } = await oracle.testGetBlockStartingAndCurrentTick(mockPool.address)
|
||||
expect(blockStartingTick).to.eq(11)
|
||||
expect(currentTick).to.eq(11)
|
||||
})
|
||||
|
||||
it('works when ticks are different in the same block', async () => {
|
||||
const mockPool = await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
const { blockStartingTick, currentTick } = await oracle.testGetBlockStartingAndCurrentTick(mockPool.address)
|
||||
expect(blockStartingTick).to.eq(11)
|
||||
expect(currentTick).to.eq(12)
|
||||
})
|
||||
|
||||
it('works when time has passed since the last block', async () => {
|
||||
const mockPool = await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await oracle.setTime(3)
|
||||
const { blockStartingTick, currentTick } = await oracle.testGetBlockStartingAndCurrentTick(mockPool.address)
|
||||
expect(blockStartingTick).to.eq(12)
|
||||
expect(currentTick).to.eq(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getSyntheticTicks(bytes,uint32)', () => {
|
||||
describe('single pool', () => {
|
||||
describe('unchanged ticks; secondsAgo = 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2), [FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(11)
|
||||
expect(syntheticCurrentTick).to.eq(11)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2).reverse(), [FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-11)
|
||||
expect(syntheticCurrentTick).to.eq(-11)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changed ticks; secondsAgo = 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2), [FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(11)
|
||||
expect(syntheticCurrentTick).to.eq(12)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2).reverse(), [FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-11)
|
||||
expect(syntheticCurrentTick).to.eq(-12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unchanged ticks; secondsAgo != 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2), [FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(11)
|
||||
expect(syntheticCurrentTick).to.eq(11)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2).reverse(), [FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-11)
|
||||
expect(syntheticCurrentTick).to.eq(-11)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changed ticks', () => {
|
||||
describe('secondsAgo = 1', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2), [FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(11)
|
||||
expect(syntheticCurrentTick).to.eq(12)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2).reverse(), [FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-11)
|
||||
expect(syntheticCurrentTick).to.eq(-12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('secondsAgo = 2', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [10, 12, 13])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2), [FeeAmount.LOW]),
|
||||
2
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(11)
|
||||
expect(syntheticCurrentTick).to.eq(13)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice(0, 2).reverse(), [FeeAmount.LOW]),
|
||||
2
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-11)
|
||||
expect(syntheticCurrentTick).to.eq(-13)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('two pools', () => {
|
||||
describe('unchanged ticks; secondsAgo = 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(22)
|
||||
expect(syntheticCurrentTick).to.eq(22)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-22)
|
||||
expect(syntheticCurrentTick).to.eq(-22)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changed ticks; secondsAgo = 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(22)
|
||||
expect(syntheticCurrentTick).to.eq(24)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
0
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-22)
|
||||
expect(syntheticCurrentTick).to.eq(-24)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unchanged ticks; secondsAgo != 0', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 11])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(22)
|
||||
expect(syntheticCurrentTick).to.eq(22)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-22)
|
||||
expect(syntheticCurrentTick).to.eq(-22)
|
||||
})
|
||||
})
|
||||
|
||||
describe('changed ticks', () => {
|
||||
describe('secondsAgo = 1', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(22)
|
||||
expect(syntheticCurrentTick).to.eq(24)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
1
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-22)
|
||||
expect(syntheticCurrentTick).to.eq(-24)
|
||||
})
|
||||
})
|
||||
|
||||
describe('secondsAgo = 2', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [10, 12, 13])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [10, 12, 13])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
2
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(22)
|
||||
expect(syntheticCurrentTick).to.eq(26)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { syntheticAverageTick, syntheticCurrentTick } = await oracle['testGetSyntheticTicks(bytes,uint32)'](
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
2
|
||||
)
|
||||
expect(syntheticAverageTick).to.eq(-22)
|
||||
expect(syntheticCurrentTick).to.eq(-26)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getSyntheticTicks(bytes[],uint128[],uint32)', () => {
|
||||
describe('same price', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[0], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 22, 24])
|
||||
})
|
||||
|
||||
it('normal order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]), encodePath([tokens[0], tokens[2]], [FeeAmount.LOW])],
|
||||
[1, 1],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(22)
|
||||
expect(averageSyntheticCurrentTick).to.eq(24)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
encodePath([tokens[2], tokens[0]], [FeeAmount.LOW]),
|
||||
],
|
||||
[1, 1],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(-22)
|
||||
expect(averageSyntheticCurrentTick).to.eq(-24)
|
||||
})
|
||||
})
|
||||
|
||||
describe('difference price', () => {
|
||||
beforeEach(async () => {
|
||||
await createMockPool(tokens[0], tokens[1], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[1], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 11, 12])
|
||||
await createMockPool(tokens[0], tokens[2], FeeAmount.LOW, [0, 1, 2], [0, 44, 48])
|
||||
})
|
||||
|
||||
describe('same weight', () => {
|
||||
it('normal order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]), encodePath([tokens[0], tokens[2]], [FeeAmount.LOW])],
|
||||
[1, 1],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(33)
|
||||
expect(averageSyntheticCurrentTick).to.eq(36)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
encodePath([tokens[2], tokens[0]], [FeeAmount.LOW]),
|
||||
],
|
||||
[1, 1],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(-33)
|
||||
expect(averageSyntheticCurrentTick).to.eq(-36)
|
||||
})
|
||||
})
|
||||
|
||||
describe('different weights', () => {
|
||||
it('normal order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[encodePath(tokens, [FeeAmount.LOW, FeeAmount.LOW]), encodePath([tokens[0], tokens[2]], [FeeAmount.LOW])],
|
||||
[1, 2],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(36)
|
||||
expect(averageSyntheticCurrentTick).to.eq(40)
|
||||
})
|
||||
|
||||
it('reverse order', async () => {
|
||||
const { averageSyntheticAverageTick, averageSyntheticCurrentTick } = await oracle[
|
||||
'testGetSyntheticTicks(bytes[],uint128[],uint32)'
|
||||
](
|
||||
[
|
||||
encodePath(tokens.slice().reverse(), [FeeAmount.LOW, FeeAmount.LOW]),
|
||||
encodePath([tokens[2], tokens[0]], [FeeAmount.LOW]),
|
||||
],
|
||||
[1, 2],
|
||||
0
|
||||
)
|
||||
|
||||
expect(averageSyntheticAverageTick).to.eq(-37)
|
||||
expect(averageSyntheticCurrentTick).to.eq(-40)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,50 @@
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { constants, Contract, ContractTransaction, Wallet } from 'ethers'
|
||||
import { waffle, ethers } from 'hardhat'
|
||||
import { IWETH9, MockTimeSwapRouter02 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { expect } from './shared/expect'
|
||||
|
||||
describe('PeripheryPaymentsExtended', function () {
|
||||
let wallet: Wallet
|
||||
|
||||
const routerFixture: Fixture<{
|
||||
weth9: IWETH9
|
||||
router: MockTimeSwapRouter02
|
||||
}> = async (wallets, provider) => {
|
||||
const { weth9, router } = await completeFixture(wallets, provider)
|
||||
|
||||
return {
|
||||
weth9,
|
||||
router,
|
||||
}
|
||||
}
|
||||
|
||||
let router: MockTimeSwapRouter02
|
||||
let weth9: IWETH9
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
;[wallet] = await (ethers as any).getSigners()
|
||||
loadFixture = waffle.createFixtureLoader([wallet])
|
||||
})
|
||||
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ weth9, router } = await loadFixture(routerFixture))
|
||||
})
|
||||
|
||||
describe('wrapETH', () => {
|
||||
it('increases router WETH9 balance by value amount', async () => {
|
||||
const value = ethers.utils.parseEther('1')
|
||||
|
||||
const weth9BalancePrev = await weth9.balanceOf(router.address)
|
||||
await router.wrapETH(value, { value })
|
||||
const weth9BalanceCurrent = await weth9.balanceOf(router.address)
|
||||
|
||||
expect(weth9BalanceCurrent.sub(weth9BalancePrev)).to.equal(value)
|
||||
expect(await weth9.balanceOf(wallet.address)).to.equal('0')
|
||||
expect(await router.provider.getBalance(router.address)).to.equal('0')
|
||||
})
|
||||
})
|
||||
})
|
||||
280
lib/swap-router-contracts/test/PoolTicksCounter.spec.ts
Normal file
280
lib/swap-router-contracts/test/PoolTicksCounter.spec.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { waffle, ethers, artifacts } from 'hardhat'
|
||||
|
||||
import { expect } from './shared/expect'
|
||||
|
||||
import { PoolTicksCounterTest } from '../typechain'
|
||||
import { deployMockContract, Fixture, MockContract } from 'ethereum-waffle'
|
||||
import { Artifact } from 'hardhat/types'
|
||||
|
||||
describe('PoolTicksCounter', () => {
|
||||
const TICK_SPACINGS = [200, 60, 10]
|
||||
|
||||
TICK_SPACINGS.forEach((TICK_SPACING) => {
|
||||
let PoolTicksCounter: PoolTicksCounterTest
|
||||
let pool: MockContract
|
||||
let PoolAbi: Artifact
|
||||
|
||||
// Bit index to tick
|
||||
const bitIdxToTick = (idx: number, page = 0) => {
|
||||
return idx * TICK_SPACING + page * 256 * TICK_SPACING
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
const wallets = await (ethers as any).getSigners()
|
||||
PoolAbi = await artifacts.readArtifact('IUniswapV3Pool')
|
||||
const poolTicksHelperFactory = await ethers.getContractFactory('PoolTicksCounterTest')
|
||||
PoolTicksCounter = (await poolTicksHelperFactory.deploy()) as PoolTicksCounterTest
|
||||
pool = await deployMockContract(wallets[0], PoolAbi.abi)
|
||||
await pool.mock.tickSpacing.returns(TICK_SPACING)
|
||||
})
|
||||
|
||||
describe(`[Tick Spacing: ${TICK_SPACING}]: tick after is bigger`, async () => {
|
||||
it('same tick initialized', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(2)
|
||||
)
|
||||
expect(result).to.be.eq(1)
|
||||
})
|
||||
|
||||
it('same tick not-initialized', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(1),
|
||||
bitIdxToTick(1)
|
||||
)
|
||||
expect(result).to.be.eq(0)
|
||||
})
|
||||
|
||||
it('same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(0),
|
||||
bitIdxToTick(255)
|
||||
)
|
||||
expect(result).to.be.eq(2)
|
||||
})
|
||||
|
||||
it('multiple pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100) // 1100
|
||||
await pool.mock.tickBitmap.withArgs(1).returns(0b1101) // 1101
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(0),
|
||||
bitIdxToTick(255, 1)
|
||||
)
|
||||
expect(result).to.be.eq(5)
|
||||
})
|
||||
|
||||
it('counts all ticks in a page except ending tick', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(ethers.constants.MaxUint256)
|
||||
await pool.mock.tickBitmap.withArgs(1).returns(0x0)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(0),
|
||||
bitIdxToTick(255, 1)
|
||||
)
|
||||
expect(result).to.be.eq(255)
|
||||
})
|
||||
|
||||
it('counts ticks to left of start and right of end on same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(8),
|
||||
bitIdxToTick(255)
|
||||
)
|
||||
expect(result).to.be.eq(4)
|
||||
})
|
||||
|
||||
it('counts ticks to left of start and right of end across on multiple pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111)
|
||||
await pool.mock.tickBitmap.withArgs(1).returns(0b1111000100001111)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(8),
|
||||
bitIdxToTick(8, 1)
|
||||
)
|
||||
expect(result).to.be.eq(9)
|
||||
})
|
||||
|
||||
it('counts ticks when before and after are initialized on same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(255)
|
||||
)
|
||||
expect(startingTickInit).to.be.eq(5)
|
||||
const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(0),
|
||||
bitIdxToTick(3)
|
||||
)
|
||||
expect(endingTickInit).to.be.eq(2)
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(5)
|
||||
)
|
||||
expect(bothInit).to.be.eq(3)
|
||||
})
|
||||
|
||||
it('counts ticks when before and after are initialized on multiple page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
await pool.mock.tickBitmap.withArgs(1).returns(0b11111100)
|
||||
const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(255)
|
||||
)
|
||||
expect(startingTickInit).to.be.eq(5)
|
||||
const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(0),
|
||||
bitIdxToTick(3, 1)
|
||||
)
|
||||
expect(endingTickInit).to.be.eq(8)
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(5, 1)
|
||||
)
|
||||
expect(bothInit).to.be.eq(9)
|
||||
})
|
||||
|
||||
it('counts ticks with lots of pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
await pool.mock.tickBitmap.withArgs(1).returns(0b11111111)
|
||||
await pool.mock.tickBitmap.withArgs(2).returns(0x0)
|
||||
await pool.mock.tickBitmap.withArgs(3).returns(0x0)
|
||||
await pool.mock.tickBitmap.withArgs(4).returns(0b11111100)
|
||||
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(4),
|
||||
bitIdxToTick(5, 4)
|
||||
)
|
||||
expect(bothInit).to.be.eq(15)
|
||||
})
|
||||
})
|
||||
|
||||
describe(`[Tick Spacing: ${TICK_SPACING}]: tick after is smaller`, async () => {
|
||||
it('same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(255),
|
||||
bitIdxToTick(0)
|
||||
)
|
||||
expect(result).to.be.eq(2)
|
||||
})
|
||||
|
||||
it('multiple pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1100)
|
||||
await pool.mock.tickBitmap.withArgs(-1).returns(0b1100)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(255),
|
||||
bitIdxToTick(0, -1)
|
||||
)
|
||||
expect(result).to.be.eq(4)
|
||||
})
|
||||
|
||||
it('counts all ticks in a page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(ethers.constants.MaxUint256)
|
||||
await pool.mock.tickBitmap.withArgs(-1).returns(0x0)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(255),
|
||||
bitIdxToTick(0, -1)
|
||||
)
|
||||
expect(result).to.be.eq(256)
|
||||
})
|
||||
|
||||
it('counts ticks to right of start and left of end on same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(15),
|
||||
bitIdxToTick(2)
|
||||
)
|
||||
expect(result).to.be.eq(6)
|
||||
})
|
||||
|
||||
it('counts ticks to right of start and left of end on multiple pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b1111000100001111)
|
||||
await pool.mock.tickBitmap.withArgs(-1).returns(0b1111000100001111)
|
||||
const result = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(8),
|
||||
bitIdxToTick(8, -1)
|
||||
)
|
||||
expect(result).to.be.eq(9)
|
||||
})
|
||||
|
||||
it('counts ticks when before and after are initialized on same page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(3),
|
||||
bitIdxToTick(0)
|
||||
)
|
||||
expect(startingTickInit).to.be.eq(2)
|
||||
const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(255),
|
||||
bitIdxToTick(2)
|
||||
)
|
||||
expect(endingTickInit).to.be.eq(5)
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(5),
|
||||
bitIdxToTick(2)
|
||||
)
|
||||
expect(bothInit).to.be.eq(3)
|
||||
})
|
||||
|
||||
it('counts ticks when before and after are initialized on multiple page', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
await pool.mock.tickBitmap.withArgs(-1).returns(0b11111100)
|
||||
const startingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(3, -1)
|
||||
)
|
||||
expect(startingTickInit).to.be.eq(5)
|
||||
const endingTickInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(5),
|
||||
bitIdxToTick(255, -1)
|
||||
)
|
||||
expect(endingTickInit).to.be.eq(4)
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(2),
|
||||
bitIdxToTick(5, -1)
|
||||
)
|
||||
expect(bothInit).to.be.eq(3)
|
||||
})
|
||||
|
||||
it('counts ticks with lots of pages', async () => {
|
||||
await pool.mock.tickBitmap.withArgs(0).returns(0b11111100)
|
||||
await pool.mock.tickBitmap.withArgs(-1).returns(0xff)
|
||||
await pool.mock.tickBitmap.withArgs(-2).returns(0x0)
|
||||
await pool.mock.tickBitmap.withArgs(-3).returns(0x0)
|
||||
await pool.mock.tickBitmap.withArgs(-4).returns(0b11111100)
|
||||
const bothInit = await PoolTicksCounter.countInitializedTicksCrossed(
|
||||
pool.address,
|
||||
bitIdxToTick(3),
|
||||
bitIdxToTick(6, -4)
|
||||
)
|
||||
expect(bothInit).to.be.eq(11)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
201
lib/swap-router-contracts/test/Quoter.spec.ts
Normal file
201
lib/swap-router-contracts/test/Quoter.spec.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { constants, Wallet, Contract } from 'ethers'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { Quoter, TestERC20 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { FeeAmount, MaxUint128, TICK_SPACINGS } from './shared/constants'
|
||||
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
||||
import { expandTo18Decimals } from './shared/expandTo18Decimals'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
import { createPool } from './shared/quoter'
|
||||
|
||||
describe('Quoter', () => {
|
||||
let wallet: Wallet
|
||||
let trader: Wallet
|
||||
|
||||
const swapRouterFixture: Fixture<{
|
||||
nft: Contract
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
quoter: Quoter
|
||||
}> = async (wallets, provider) => {
|
||||
const { weth9, factory, router, tokens, nft } = await completeFixture(wallets, provider)
|
||||
|
||||
// approve & fund wallets
|
||||
for (const token of tokens) {
|
||||
await token.approve(router.address, constants.MaxUint256)
|
||||
await token.approve(nft.address, constants.MaxUint256)
|
||||
await token.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
await token.transfer(trader.address, expandTo18Decimals(1_000_000))
|
||||
}
|
||||
|
||||
const quoterFactory = await ethers.getContractFactory('Quoter')
|
||||
quoter = (await quoterFactory.deploy(factory.address, weth9.address)) as Quoter
|
||||
|
||||
return {
|
||||
tokens,
|
||||
nft,
|
||||
quoter,
|
||||
}
|
||||
}
|
||||
|
||||
let nft: Contract
|
||||
let tokens: [TestERC20, TestERC20, TestERC20]
|
||||
let quoter: Quoter
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
const wallets = await (ethers as any).getSigners()
|
||||
;[wallet, trader] = wallets
|
||||
loadFixture = waffle.createFixtureLoader(wallets)
|
||||
})
|
||||
|
||||
// helper for getting weth and token balances
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ tokens, nft, quoter } = await loadFixture(swapRouterFixture))
|
||||
})
|
||||
|
||||
describe('quotes', () => {
|
||||
beforeEach(async () => {
|
||||
await createPool(nft, wallet, tokens[0].address, tokens[1].address)
|
||||
await createPool(nft, wallet, tokens[1].address, tokens[2].address)
|
||||
})
|
||||
|
||||
describe('#quoteExactInput', () => {
|
||||
it('0 -> 1', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
||||
3
|
||||
)
|
||||
|
||||
expect(quote).to.eq(1)
|
||||
})
|
||||
|
||||
it('1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
3
|
||||
)
|
||||
|
||||
expect(quote).to.eq(1)
|
||||
})
|
||||
|
||||
it('0 -> 1 -> 2', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInput(
|
||||
encodePath(
|
||||
tokens.map((token) => token.address),
|
||||
[FeeAmount.MEDIUM, FeeAmount.MEDIUM]
|
||||
),
|
||||
5
|
||||
)
|
||||
|
||||
expect(quote).to.eq(1)
|
||||
})
|
||||
|
||||
it('2 -> 1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInput(
|
||||
encodePath(tokens.map((token) => token.address).reverse(), [FeeAmount.MEDIUM, FeeAmount.MEDIUM]),
|
||||
5
|
||||
)
|
||||
|
||||
expect(quote).to.eq(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactInputSingle', () => {
|
||||
it('0 -> 1', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInputSingle(
|
||||
tokens[0].address,
|
||||
tokens[1].address,
|
||||
FeeAmount.MEDIUM,
|
||||
MaxUint128,
|
||||
// -2%
|
||||
encodePriceSqrt(100, 102)
|
||||
)
|
||||
|
||||
expect(quote).to.eq(9852)
|
||||
})
|
||||
|
||||
it('1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactInputSingle(
|
||||
tokens[1].address,
|
||||
tokens[0].address,
|
||||
FeeAmount.MEDIUM,
|
||||
MaxUint128,
|
||||
// +2%
|
||||
encodePriceSqrt(102, 100)
|
||||
)
|
||||
|
||||
expect(quote).to.eq(9852)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactOutput', () => {
|
||||
it('0 -> 1', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
1
|
||||
)
|
||||
|
||||
expect(quote).to.eq(3)
|
||||
})
|
||||
|
||||
it('1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
||||
1
|
||||
)
|
||||
|
||||
expect(quote).to.eq(3)
|
||||
})
|
||||
|
||||
it('0 -> 1 -> 2', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath(tokens.map((token) => token.address).reverse(), [FeeAmount.MEDIUM, FeeAmount.MEDIUM]),
|
||||
1
|
||||
)
|
||||
|
||||
expect(quote).to.eq(5)
|
||||
})
|
||||
|
||||
it('2 -> 1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath(
|
||||
tokens.map((token) => token.address),
|
||||
[FeeAmount.MEDIUM, FeeAmount.MEDIUM]
|
||||
),
|
||||
1
|
||||
)
|
||||
|
||||
expect(quote).to.eq(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactOutputSingle', () => {
|
||||
it('0 -> 1', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutputSingle(
|
||||
tokens[0].address,
|
||||
tokens[1].address,
|
||||
FeeAmount.MEDIUM,
|
||||
MaxUint128,
|
||||
encodePriceSqrt(100, 102)
|
||||
)
|
||||
|
||||
expect(quote).to.eq(9981)
|
||||
})
|
||||
|
||||
it('1 -> 0', async () => {
|
||||
const quote = await quoter.callStatic.quoteExactOutputSingle(
|
||||
tokens[1].address,
|
||||
tokens[0].address,
|
||||
FeeAmount.MEDIUM,
|
||||
MaxUint128,
|
||||
encodePriceSqrt(102, 100)
|
||||
)
|
||||
|
||||
expect(quote).to.eq(9981)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
578
lib/swap-router-contracts/test/QuoterV2.spec.ts
Normal file
578
lib/swap-router-contracts/test/QuoterV2.spec.ts
Normal file
@ -0,0 +1,578 @@
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { constants, Wallet, Contract } from 'ethers'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { QuoterV2, TestERC20 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { FeeAmount, MaxUint128 } from './shared/constants'
|
||||
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
||||
import { expandTo18Decimals } from './shared/expandTo18Decimals'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
import { createPool, createPoolWithMultiplePositions, createPoolWithZeroTickInitialized } from './shared/quoter'
|
||||
import snapshotGasCost from './shared/snapshotGasCost'
|
||||
|
||||
describe('QuoterV2', function () {
|
||||
this.timeout(40000)
|
||||
let wallet: Wallet
|
||||
let trader: Wallet
|
||||
|
||||
const swapRouterFixture: Fixture<{
|
||||
nft: Contract
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
quoter: QuoterV2
|
||||
}> = async (wallets, provider) => {
|
||||
const { weth9, factory, router, tokens, nft } = await completeFixture(wallets, provider)
|
||||
|
||||
// approve & fund wallets
|
||||
for (const token of tokens) {
|
||||
await token.approve(router.address, constants.MaxUint256)
|
||||
await token.approve(nft.address, constants.MaxUint256)
|
||||
await token.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
await token.transfer(trader.address, expandTo18Decimals(1_000_000))
|
||||
}
|
||||
|
||||
const quoterFactory = await ethers.getContractFactory('QuoterV2')
|
||||
quoter = (await quoterFactory.deploy(factory.address, weth9.address)) as QuoterV2
|
||||
|
||||
return {
|
||||
tokens,
|
||||
nft,
|
||||
quoter,
|
||||
}
|
||||
}
|
||||
|
||||
let nft: Contract
|
||||
let tokens: [TestERC20, TestERC20, TestERC20]
|
||||
let quoter: QuoterV2
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
const wallets = await (ethers as any).getSigners()
|
||||
;[wallet, trader] = wallets
|
||||
loadFixture = waffle.createFixtureLoader(wallets)
|
||||
})
|
||||
|
||||
// helper for getting weth and token balances
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ tokens, nft, quoter } = await loadFixture(swapRouterFixture))
|
||||
})
|
||||
|
||||
describe('quotes', () => {
|
||||
beforeEach(async () => {
|
||||
await createPool(nft, wallet, tokens[0].address, tokens[1].address)
|
||||
await createPool(nft, wallet, tokens[1].address, tokens[2].address)
|
||||
await createPoolWithMultiplePositions(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
})
|
||||
|
||||
describe('#quoteExactInput', () => {
|
||||
it('0 -> 2 cross 2 tick', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(amountOut).to.eq(9871)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 2 tick where after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is -120.
|
||||
// -120 is an initialized tick for this pool. We check that we don't count it.
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
6200
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78757224507315167622282810783')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(amountOut).to.eq(6143)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 1 tick', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
4000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78926452400586371254602774705')
|
||||
expect(amountOut).to.eq(3971)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick, starting tick not initialized', async () => {
|
||||
// Tick before 0, tick after -1.
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79227483487511329217250071027')
|
||||
expect(amountOut).to.eq(8)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick, starting tick initialized', async () => {
|
||||
// Tick before 0, tick after -1. Tick 0 initialized.
|
||||
await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
10
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79227817515327498931091950511')
|
||||
expect(amountOut).to.eq(8)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('80001962924147897865541384515')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(9871)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2 where tick after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is 120.
|
||||
// 120 is an initialized tick for this pool. We check we don't count it.
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
6250
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79705728824507063507279123685')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(6190)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 0 tick, starting tick initialized', async () => {
|
||||
// Tick 0 initialized. Tick after = 1
|
||||
await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
200
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79235729830182478001034429156')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(198)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 0 tick, starting tick not initialized', async () => {
|
||||
// Tick 0 initialized. Tick after = 1
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
103
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79235858216754624215638319723')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountOut).to.eq(101)
|
||||
})
|
||||
|
||||
it('2 -> 1', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('80018067294531553039351583520')
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(amountOut).to.eq(9871)
|
||||
})
|
||||
|
||||
it('0 -> 2 -> 1', async () => {
|
||||
const {
|
||||
amountOut,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInput(
|
||||
encodePath([tokens[0].address, tokens[2].address, tokens[1].address], [FeeAmount.MEDIUM, FeeAmount.MEDIUM]),
|
||||
10000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(2)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78461846509168490764501028180')
|
||||
expect(sqrtPriceX96AfterList[1]).to.eq('80007846861567212939802016351')
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(initializedTicksCrossedList[1]).to.eq(0)
|
||||
expect(amountOut).to.eq(9745)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactInputSingle', () => {
|
||||
it('0 -> 2', async () => {
|
||||
const {
|
||||
amountOut: quote,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInputSingle({
|
||||
tokenIn: tokens[0].address,
|
||||
tokenOut: tokens[2].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amountIn: 10000,
|
||||
// -2%
|
||||
sqrtPriceLimitX96: encodePriceSqrt(100, 102),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossed).to.eq(2)
|
||||
expect(quote).to.eq(9871)
|
||||
expect(sqrtPriceX96After).to.eq('78461846509168490764501028180')
|
||||
})
|
||||
|
||||
it('2 -> 0', async () => {
|
||||
const {
|
||||
amountOut: quote,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactInputSingle({
|
||||
tokenIn: tokens[2].address,
|
||||
tokenOut: tokens[0].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amountIn: 10000,
|
||||
// +2%
|
||||
sqrtPriceLimitX96: encodePriceSqrt(102, 100),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossed).to.eq(2)
|
||||
expect(quote).to.eq(9871)
|
||||
expect(sqrtPriceX96After).to.eq('80001962924147897865541384515')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactOutput', () => {
|
||||
it('0 -> 2 cross 2 tick', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
15000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(amountIn).to.eq(15273)
|
||||
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78055527257643669242286029831')
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 2 where tick after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is -120.
|
||||
// -120 is an initialized tick for this pool. We check that we count it.
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
6143
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78757225449310403327341205211')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(amountIn).to.eq(6200)
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 1 tick', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
4000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(amountIn).to.eq(4029)
|
||||
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('78924219757724709840818372098')
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick starting tick initialized', async () => {
|
||||
// Tick before 0, tick after 1. Tick 0 initialized.
|
||||
await createPoolWithZeroTickInitialized(nft, wallet, tokens[0].address, tokens[2].address)
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
100
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(amountIn).to.eq(102)
|
||||
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79224329176051641448521403903')
|
||||
})
|
||||
|
||||
it('0 -> 2 cross 0 tick starting tick not initialized', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[2].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
10
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(amountIn).to.eq(12)
|
||||
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79227408033628034983534698435')
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2 ticks', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
15000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(amountIn).to.eq(15273)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('80418414376567919517220409857')
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 2 where tick after is initialized', async () => {
|
||||
// The swap amount is set such that the active tick after the swap is 120.
|
||||
// 120 is an initialized tick for this pool. We check that we don't count it.
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
6223
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(2)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79708304437530892332449657932')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountIn).to.eq(6283)
|
||||
})
|
||||
|
||||
it('2 -> 0 cross 1 tick', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[0].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
6000
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(initializedTicksCrossedList[0]).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('79690640184021170956740081887')
|
||||
expect(initializedTicksCrossedList.length).to.eq(1)
|
||||
expect(amountIn).to.eq(6055)
|
||||
})
|
||||
|
||||
it('2 -> 1', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[1].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
9871
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(1)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('80018020393569259756601362385')
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(amountIn).to.eq(10000)
|
||||
})
|
||||
|
||||
it('0 -> 2 -> 1', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96AfterList,
|
||||
initializedTicksCrossedList,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutput(
|
||||
encodePath([tokens[0].address, tokens[2].address, tokens[1].address].reverse(), [
|
||||
FeeAmount.MEDIUM,
|
||||
FeeAmount.MEDIUM,
|
||||
]),
|
||||
9745
|
||||
)
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(sqrtPriceX96AfterList.length).to.eq(2)
|
||||
expect(sqrtPriceX96AfterList[0]).to.eq('80007838904387594703933785072')
|
||||
expect(sqrtPriceX96AfterList[1]).to.eq('78461888503179331029803316753')
|
||||
expect(initializedTicksCrossedList[0]).to.eq(0)
|
||||
expect(initializedTicksCrossedList[1]).to.eq(2)
|
||||
expect(amountIn).to.eq(10000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#quoteExactOutputSingle', () => {
|
||||
it('0 -> 1', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutputSingle({
|
||||
tokenIn: tokens[0].address,
|
||||
tokenOut: tokens[1].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amount: MaxUint128,
|
||||
sqrtPriceLimitX96: encodePriceSqrt(100, 102),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(amountIn).to.eq(9981)
|
||||
expect(initializedTicksCrossed).to.eq(0)
|
||||
expect(sqrtPriceX96After).to.eq('78447570448055484695608110440')
|
||||
})
|
||||
|
||||
it('1 -> 0', async () => {
|
||||
const {
|
||||
amountIn,
|
||||
sqrtPriceX96After,
|
||||
initializedTicksCrossed,
|
||||
gasEstimate,
|
||||
} = await quoter.callStatic.quoteExactOutputSingle({
|
||||
tokenIn: tokens[1].address,
|
||||
tokenOut: tokens[0].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
amount: MaxUint128,
|
||||
sqrtPriceLimitX96: encodePriceSqrt(102, 100),
|
||||
})
|
||||
|
||||
await snapshotGasCost(gasEstimate)
|
||||
expect(amountIn).to.eq(9981)
|
||||
expect(initializedTicksCrossed).to.eq(0)
|
||||
expect(sqrtPriceX96After).to.eq('80016521857016594389520272648')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
470
lib/swap-router-contracts/test/SwapRouter.gas.spec.ts
Normal file
470
lib/swap-router-contracts/test/SwapRouter.gas.spec.ts
Normal file
@ -0,0 +1,470 @@
|
||||
import { defaultAbiCoder } from '@ethersproject/abi'
|
||||
import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json'
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { BigNumber, constants, ContractTransaction, Wallet } from 'ethers'
|
||||
import { solidityPack } from 'ethers/lib/utils'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { IUniswapV3Pool, IWETH9, MockTimeSwapRouter02, TestERC20 } from '../typechain'
|
||||
import completeFixture from './shared/completeFixture'
|
||||
import { ADDRESS_THIS, FeeAmount, MSG_SENDER, TICK_SPACINGS } from './shared/constants'
|
||||
import { encodePriceSqrt } from './shared/encodePriceSqrt'
|
||||
import { expandTo18Decimals } from './shared/expandTo18Decimals'
|
||||
import { expect } from './shared/expect'
|
||||
import { encodePath } from './shared/path'
|
||||
import snapshotGasCost from './shared/snapshotGasCost'
|
||||
import { getMaxTick, getMinTick } from './shared/ticks'
|
||||
|
||||
describe('SwapRouter gas tests', function () {
|
||||
this.timeout(40000)
|
||||
let wallet: Wallet
|
||||
let trader: Wallet
|
||||
|
||||
const swapRouterFixture: Fixture<{
|
||||
weth9: IWETH9
|
||||
router: MockTimeSwapRouter02
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
pools: [IUniswapV3Pool, IUniswapV3Pool, IUniswapV3Pool]
|
||||
}> = async (wallets, provider) => {
|
||||
const { weth9, factory, router, tokens, nft } = await completeFixture(wallets, provider)
|
||||
|
||||
// approve & fund wallets
|
||||
for (const token of tokens) {
|
||||
await token.approve(router.address, constants.MaxUint256)
|
||||
await token.approve(nft.address, constants.MaxUint256)
|
||||
await token.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
await token.transfer(trader.address, expandTo18Decimals(1_000_000))
|
||||
}
|
||||
|
||||
const liquidity = 1000000
|
||||
async function createPool(tokenAddressA: string, tokenAddressB: string) {
|
||||
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
|
||||
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
|
||||
|
||||
await nft.createAndInitializePoolIfNecessary(
|
||||
tokenAddressA,
|
||||
tokenAddressB,
|
||||
FeeAmount.MEDIUM,
|
||||
encodePriceSqrt(100005, 100000) // we don't want to cross any ticks
|
||||
)
|
||||
|
||||
const liquidityParams = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 1000000,
|
||||
amount1Desired: 1000000,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
return nft.mint(liquidityParams)
|
||||
}
|
||||
|
||||
async function createPoolWETH9(tokenAddress: string) {
|
||||
await weth9.deposit({ value: liquidity * 2 })
|
||||
await weth9.approve(nft.address, constants.MaxUint256)
|
||||
return createPool(weth9.address, tokenAddress)
|
||||
}
|
||||
|
||||
// create pools
|
||||
await createPool(tokens[0].address, tokens[1].address)
|
||||
await createPool(tokens[1].address, tokens[2].address)
|
||||
await createPoolWETH9(tokens[0].address)
|
||||
|
||||
const poolAddresses = await Promise.all([
|
||||
factory.getPool(tokens[0].address, tokens[1].address, FeeAmount.MEDIUM),
|
||||
factory.getPool(tokens[1].address, tokens[2].address, FeeAmount.MEDIUM),
|
||||
factory.getPool(weth9.address, tokens[0].address, FeeAmount.MEDIUM),
|
||||
])
|
||||
|
||||
const pools = poolAddresses.map((poolAddress) => new ethers.Contract(poolAddress, IUniswapV3PoolABI, wallet)) as [
|
||||
IUniswapV3Pool,
|
||||
IUniswapV3Pool,
|
||||
IUniswapV3Pool
|
||||
]
|
||||
|
||||
return {
|
||||
weth9,
|
||||
router,
|
||||
tokens,
|
||||
pools,
|
||||
}
|
||||
}
|
||||
|
||||
let weth9: IWETH9
|
||||
let router: MockTimeSwapRouter02
|
||||
let tokens: [TestERC20, TestERC20, TestERC20]
|
||||
let pools: [IUniswapV3Pool, IUniswapV3Pool, IUniswapV3Pool]
|
||||
|
||||
let loadFixture: ReturnType<typeof waffle.createFixtureLoader>
|
||||
|
||||
function encodeUnwrapWETH9(amount: number) {
|
||||
return solidityPack(
|
||||
['bytes4', 'bytes'],
|
||||
[router.interface.getSighash('unwrapWETH9(uint256)'), defaultAbiCoder.encode(['uint256'], [amount])]
|
||||
)
|
||||
}
|
||||
|
||||
function encodeSweep(token: string, amount: number) {
|
||||
const functionSignature = 'sweepToken(address,uint256)'
|
||||
return solidityPack(
|
||||
['bytes4', 'bytes'],
|
||||
[
|
||||
router.interface.getSighash(functionSignature),
|
||||
defaultAbiCoder.encode((router.interface.functions as any)[functionSignature].inputs, [token, amount]),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
before('create fixture loader', async () => {
|
||||
const wallets = await (ethers as any).getSigners()
|
||||
;[wallet, trader] = wallets
|
||||
|
||||
loadFixture = waffle.createFixtureLoader(wallets)
|
||||
})
|
||||
|
||||
beforeEach('load fixture', async () => {
|
||||
;({ router, weth9, tokens, pools } = await loadFixture(swapRouterFixture))
|
||||
})
|
||||
|
||||
async function exactInput(
|
||||
tokens: string[],
|
||||
amountIn: number = 2,
|
||||
amountOutMinimum: number = 1
|
||||
): Promise<ContractTransaction> {
|
||||
const inputIsWETH = weth9.address === tokens[0]
|
||||
const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address
|
||||
|
||||
const value = inputIsWETH ? amountIn : 0
|
||||
|
||||
const params = {
|
||||
path: encodePath(tokens, new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)),
|
||||
recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER,
|
||||
amountIn,
|
||||
amountOutMinimum: outputIsWETH9 ? 0 : amountOutMinimum, // save on calldata
|
||||
}
|
||||
|
||||
const data = [router.interface.encodeFunctionData('exactInput', [params])]
|
||||
if (outputIsWETH9) {
|
||||
data.push(encodeUnwrapWETH9(amountOutMinimum))
|
||||
}
|
||||
|
||||
return router.connect(trader)['multicall(uint256,bytes[])'](1, data, { value })
|
||||
}
|
||||
|
||||
async function exactInputSingle(
|
||||
tokenIn: string,
|
||||
tokenOut: string,
|
||||
amountIn: number = 3,
|
||||
amountOutMinimum: number = 1,
|
||||
sqrtPriceLimitX96?: BigNumber
|
||||
): Promise<ContractTransaction> {
|
||||
const inputIsWETH = weth9.address === tokenIn
|
||||
const outputIsWETH9 = tokenOut === weth9.address
|
||||
|
||||
const value = inputIsWETH ? amountIn : 0
|
||||
|
||||
const params = {
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER,
|
||||
amountIn,
|
||||
amountOutMinimum: outputIsWETH9 ? 0 : amountOutMinimum, // save on calldata
|
||||
sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0,
|
||||
}
|
||||
|
||||
const data = [router.interface.encodeFunctionData('exactInputSingle', [params])]
|
||||
if (outputIsWETH9) {
|
||||
data.push(encodeUnwrapWETH9(amountOutMinimum))
|
||||
}
|
||||
|
||||
return router.connect(trader)['multicall(uint256,bytes[])'](1, data, { value })
|
||||
}
|
||||
|
||||
async function exactOutput(tokens: string[]): Promise<ContractTransaction> {
|
||||
const amountInMaximum = 10 // we don't care
|
||||
const amountOut = 1
|
||||
|
||||
const inputIsWETH9 = tokens[0] === weth9.address
|
||||
const outputIsWETH9 = tokens[tokens.length - 1] === weth9.address
|
||||
|
||||
const value = inputIsWETH9 ? amountInMaximum : 0
|
||||
|
||||
const params = {
|
||||
path: encodePath(tokens.slice().reverse(), new Array(tokens.length - 1).fill(FeeAmount.MEDIUM)),
|
||||
recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER,
|
||||
amountOut,
|
||||
amountInMaximum,
|
||||
}
|
||||
|
||||
const data = [router.interface.encodeFunctionData('exactOutput', [params])]
|
||||
if (inputIsWETH9) {
|
||||
data.push(router.interface.encodeFunctionData('refundETH'))
|
||||
}
|
||||
|
||||
if (outputIsWETH9) {
|
||||
data.push(encodeUnwrapWETH9(amountOut))
|
||||
}
|
||||
|
||||
return router.connect(trader)['multicall(uint256,bytes[])'](1, data, { value })
|
||||
}
|
||||
|
||||
async function exactOutputSingle(
|
||||
tokenIn: string,
|
||||
tokenOut: string,
|
||||
amountOut: number = 1,
|
||||
amountInMaximum: number = 3,
|
||||
sqrtPriceLimitX96?: BigNumber
|
||||
): Promise<ContractTransaction> {
|
||||
const inputIsWETH9 = tokenIn === weth9.address
|
||||
const outputIsWETH9 = tokenOut === weth9.address
|
||||
|
||||
const value = inputIsWETH9 ? amountInMaximum : 0
|
||||
|
||||
const params = {
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
recipient: outputIsWETH9 ? ADDRESS_THIS : MSG_SENDER,
|
||||
amountOut,
|
||||
amountInMaximum,
|
||||
sqrtPriceLimitX96: sqrtPriceLimitX96 ?? 0,
|
||||
}
|
||||
|
||||
const data = [router.interface.encodeFunctionData('exactOutputSingle', [params])]
|
||||
if (inputIsWETH9) {
|
||||
data.push(router.interface.encodeFunctionData('refundETH'))
|
||||
}
|
||||
|
||||
if (outputIsWETH9) {
|
||||
data.push(encodeUnwrapWETH9(amountOut))
|
||||
}
|
||||
|
||||
return router.connect(trader)['multicall(uint256,bytes[])'](1, data, { value })
|
||||
}
|
||||
|
||||
// TODO should really throw this in the fixture
|
||||
beforeEach('intialize feeGrowthGlobals', async () => {
|
||||
await exactInput([tokens[0].address, tokens[1].address], 1, 0)
|
||||
await exactInput([tokens[1].address, tokens[0].address], 1, 0)
|
||||
await exactInput([tokens[1].address, tokens[2].address], 1, 0)
|
||||
await exactInput([tokens[2].address, tokens[1].address], 1, 0)
|
||||
await exactInput([tokens[0].address, weth9.address], 1, 0)
|
||||
await exactInput([weth9.address, tokens[0].address], 1, 0)
|
||||
})
|
||||
|
||||
beforeEach('ensure feeGrowthGlobals are >0', async () => {
|
||||
const slots = await Promise.all(
|
||||
pools.map((pool) =>
|
||||
Promise.all([
|
||||
pool.feeGrowthGlobal0X128().then((f) => f.toString()),
|
||||
pool.feeGrowthGlobal1X128().then((f) => f.toString()),
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
expect(slots).to.deep.eq([
|
||||
['340290874192793283295456993856614', '340290874192793283295456993856614'],
|
||||
['340290874192793283295456993856614', '340290874192793283295456993856614'],
|
||||
['340290874192793283295456993856614', '340290874192793283295456993856614'],
|
||||
])
|
||||
})
|
||||
|
||||
beforeEach('ensure ticks are 0 before', async () => {
|
||||
const slots = await Promise.all(pools.map((pool) => pool.slot0().then(({ tick }) => tick)))
|
||||
expect(slots).to.deep.eq([0, 0, 0])
|
||||
})
|
||||
|
||||
afterEach('ensure ticks are 0 after', async () => {
|
||||
const slots = await Promise.all(pools.map((pool) => pool.slot0().then(({ tick }) => tick)))
|
||||
expect(slots).to.deep.eq([0, 0, 0])
|
||||
})
|
||||
|
||||
describe('#exactInput', () => {
|
||||
it('0 -> 1', async () => {
|
||||
await snapshotGasCost(exactInput(tokens.slice(0, 2).map((token) => token.address)))
|
||||
})
|
||||
|
||||
it('0 -> 1 minimal', async () => {
|
||||
const calleeFactory = await ethers.getContractFactory('TestUniswapV3Callee')
|
||||
const callee = await calleeFactory.deploy()
|
||||
|
||||
await tokens[0].connect(trader).approve(callee.address, constants.MaxUint256)
|
||||
await snapshotGasCost(callee.connect(trader).swapExact0For1(pools[0].address, 2, trader.address, '4295128740'))
|
||||
})
|
||||
|
||||
it('0 -> 1 -> 2', async () => {
|
||||
await snapshotGasCost(
|
||||
exactInput(
|
||||
tokens.map((token) => token.address),
|
||||
3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('WETH9 -> 0', async () => {
|
||||
await snapshotGasCost(
|
||||
exactInput(
|
||||
[weth9.address, tokens[0].address],
|
||||
weth9.address.toLowerCase() < tokens[0].address.toLowerCase() ? 2 : 3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('0 -> WETH9', async () => {
|
||||
await snapshotGasCost(
|
||||
exactInput(
|
||||
[tokens[0].address, weth9.address],
|
||||
tokens[0].address.toLowerCase() < weth9.address.toLowerCase() ? 2 : 3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('2 trades (via router)', async () => {
|
||||
await weth9.connect(trader).deposit({ value: 3 })
|
||||
await weth9.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
const swap0 = {
|
||||
path: encodePath([weth9.address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
recipient: ADDRESS_THIS,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 0, // save on calldata
|
||||
}
|
||||
|
||||
const swap1 = {
|
||||
path: encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
recipient: ADDRESS_THIS,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 0, // save on calldata
|
||||
}
|
||||
|
||||
const data = [
|
||||
router.interface.encodeFunctionData('exactInput', [swap0]),
|
||||
router.interface.encodeFunctionData('exactInput', [swap1]),
|
||||
encodeSweep(tokens[0].address, 2),
|
||||
]
|
||||
|
||||
await snapshotGasCost(router.connect(trader)['multicall(uint256,bytes[])'](1, data))
|
||||
})
|
||||
|
||||
it('2 trades (directly to sender)', async () => {
|
||||
await weth9.connect(trader).deposit({ value: 3 })
|
||||
await weth9.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
const swap0 = {
|
||||
path: encodePath([weth9.address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
recipient: MSG_SENDER,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 1,
|
||||
}
|
||||
|
||||
const swap1 = {
|
||||
path: encodePath([tokens[1].address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
recipient: MSG_SENDER,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 1,
|
||||
}
|
||||
|
||||
const data = [
|
||||
router.interface.encodeFunctionData('exactInput', [swap0]),
|
||||
router.interface.encodeFunctionData('exactInput', [swap1]),
|
||||
]
|
||||
|
||||
await snapshotGasCost(router.connect(trader)['multicall(uint256,bytes[])'](1, data))
|
||||
})
|
||||
|
||||
it('3 trades (directly to sender)', async () => {
|
||||
await weth9.connect(trader).deposit({ value: 3 })
|
||||
await weth9.connect(trader).approve(router.address, constants.MaxUint256)
|
||||
const swap0 = {
|
||||
path: encodePath([weth9.address, tokens[0].address], [FeeAmount.MEDIUM]),
|
||||
recipient: MSG_SENDER,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 1,
|
||||
}
|
||||
|
||||
const swap1 = {
|
||||
path: encodePath([tokens[0].address, tokens[1].address], [FeeAmount.MEDIUM]),
|
||||
recipient: MSG_SENDER,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 1,
|
||||
}
|
||||
|
||||
const swap2 = {
|
||||
path: encodePath([tokens[1].address, tokens[2].address], [FeeAmount.MEDIUM]),
|
||||
recipient: MSG_SENDER,
|
||||
amountIn: 3,
|
||||
amountOutMinimum: 1,
|
||||
}
|
||||
|
||||
const data = [
|
||||
router.interface.encodeFunctionData('exactInput', [swap0]),
|
||||
router.interface.encodeFunctionData('exactInput', [swap1]),
|
||||
router.interface.encodeFunctionData('exactInput', [swap2]),
|
||||
]
|
||||
|
||||
await snapshotGasCost(router.connect(trader)['multicall(uint256,bytes[])'](1, data))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#exactInputSingle', () => {
|
||||
it('0 -> 1', async () => {
|
||||
await snapshotGasCost(exactInputSingle(tokens[0].address, tokens[1].address))
|
||||
})
|
||||
|
||||
it('WETH9 -> 0', async () => {
|
||||
await snapshotGasCost(
|
||||
exactInputSingle(
|
||||
weth9.address,
|
||||
tokens[0].address,
|
||||
weth9.address.toLowerCase() < tokens[0].address.toLowerCase() ? 2 : 3
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('0 -> WETH9', async () => {
|
||||
await snapshotGasCost(
|
||||
exactInputSingle(
|
||||
tokens[0].address,
|
||||
weth9.address,
|
||||
tokens[0].address.toLowerCase() < weth9.address.toLowerCase() ? 2 : 3
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#exactOutput', () => {
|
||||
it('0 -> 1', async () => {
|
||||
await snapshotGasCost(exactOutput(tokens.slice(0, 2).map((token) => token.address)))
|
||||
})
|
||||
|
||||
it('0 -> 1 -> 2', async () => {
|
||||
await snapshotGasCost(exactOutput(tokens.map((token) => token.address)))
|
||||
})
|
||||
|
||||
it('WETH9 -> 0', async () => {
|
||||
await snapshotGasCost(exactOutput([weth9.address, tokens[0].address]))
|
||||
})
|
||||
|
||||
it('0 -> WETH9', async () => {
|
||||
await snapshotGasCost(exactOutput([tokens[0].address, weth9.address]))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#exactOutputSingle', () => {
|
||||
it('0 -> 1', async () => {
|
||||
await snapshotGasCost(exactOutputSingle(tokens[0].address, tokens[1].address))
|
||||
})
|
||||
|
||||
it('WETH9 -> 0', async () => {
|
||||
await snapshotGasCost(exactOutputSingle(weth9.address, tokens[0].address))
|
||||
})
|
||||
|
||||
it('0 -> WETH9', async () => {
|
||||
await snapshotGasCost(exactOutputSingle(tokens[0].address, weth9.address))
|
||||
})
|
||||
})
|
||||
})
|
||||
1623
lib/swap-router-contracts/test/SwapRouter.spec.ts
Normal file
1623
lib/swap-router-contracts/test/SwapRouter.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
158
lib/swap-router-contracts/test/TokenValidator.spec.ts
Normal file
158
lib/swap-router-contracts/test/TokenValidator.spec.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { expect } from 'chai'
|
||||
import { constants } from 'ethers'
|
||||
import hre, { ethers } from 'hardhat'
|
||||
import { TokenValidator, TestERC20, IUniswapV2Pair__factory } from '../typechain'
|
||||
|
||||
describe('TokenValidator', function () {
|
||||
let tokenValidator: TokenValidator
|
||||
let testToken: TestERC20
|
||||
|
||||
this.timeout(100000)
|
||||
|
||||
enum Status {
|
||||
UNKN = 0,
|
||||
FOT = 1,
|
||||
STF = 2,
|
||||
}
|
||||
|
||||
// WETH9 and USDC
|
||||
const BASE_TOKENS = ['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48']
|
||||
// Arbitrary amount to flash loan.
|
||||
const AMOUNT_TO_BORROW = 1000
|
||||
|
||||
const FOT_TOKENS = [
|
||||
'0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
|
||||
'0x8B3192f5eEBD8579568A2Ed41E6FEB402f93f73F', // SAITAMA
|
||||
'0xA2b4C0Af19cC16a6CfAcCe81F192B024d625817D', // KISHU
|
||||
]
|
||||
|
||||
const BROKEN_TOKENS = [
|
||||
'0xd233d1f6fd11640081abb8db125f722b5dc729dc', // USD
|
||||
]
|
||||
|
||||
const NON_FOT_TOKENS = [
|
||||
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
|
||||
'0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', // UNI
|
||||
'0xc00e94Cb662C3520282E6f5717214004A7f26888', // COMP
|
||||
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH9
|
||||
]
|
||||
|
||||
before(async function () {
|
||||
// Easiest to test FOT using real world data, so these tests require a hardhat fork.
|
||||
if (!process.env.ARCHIVE_RPC_URL) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
await hre.network.provider.request({
|
||||
method: 'hardhat_reset',
|
||||
params: [
|
||||
{
|
||||
forking: {
|
||||
jsonRpcUrl: process.env.ARCHIVE_RPC_URL,
|
||||
blockNumber: 14024832,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const factory = await ethers.getContractFactory('TokenValidator')
|
||||
tokenValidator = (await factory.deploy(
|
||||
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f', // V2 Factory
|
||||
'0xC36442b4a4522E871399CD717aBDD847Ab11FE88' // V3 NFT position manager
|
||||
)) as TokenValidator
|
||||
|
||||
// Deploy a new token for testing.
|
||||
const tokenFactory = await ethers.getContractFactory('TestERC20')
|
||||
testToken = (await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Disable mainnet forking to avoid effecting other tests.
|
||||
await hre.network.provider.request({
|
||||
method: 'hardhat_reset',
|
||||
params: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds for tokens that cant be transferred', async () => {
|
||||
for (const token of BROKEN_TOKENS) {
|
||||
const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW)
|
||||
expect(isFot).to.equal(Status.STF)
|
||||
}
|
||||
})
|
||||
|
||||
it('succeeds to detect fot tokens', async () => {
|
||||
for (const token of FOT_TOKENS) {
|
||||
const isFot = await tokenValidator.callStatic.validate(token, [BASE_TOKENS[0]!], AMOUNT_TO_BORROW)
|
||||
expect(isFot).to.equal(Status.FOT)
|
||||
}
|
||||
})
|
||||
|
||||
it('succeeds to detect fot token when token doesnt have pair with first base token', async () => {
|
||||
const isFot = await tokenValidator.callStatic.validate(
|
||||
FOT_TOKENS[0],
|
||||
[testToken.address, ...BASE_TOKENS],
|
||||
AMOUNT_TO_BORROW
|
||||
)
|
||||
expect(isFot).to.equal(Status.FOT)
|
||||
})
|
||||
|
||||
it('succeeds to return unknown when flash loaning full reserves', async () => {
|
||||
const pairAddress = '0xab293dce330b92aa52bc2a7cd3816edaa75f890b' // WTF/ETH pair
|
||||
const pair = IUniswapV2Pair__factory.connect(pairAddress, ethers.provider)
|
||||
const { reserve0: wtfReserve } = await pair.callStatic.getReserves()
|
||||
|
||||
const isFot1 = await tokenValidator.callStatic.validate(
|
||||
'0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
|
||||
['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // WETH
|
||||
wtfReserve.sub(1).toString()
|
||||
)
|
||||
expect(isFot1).to.equal(Status.FOT)
|
||||
|
||||
const isFot2 = await tokenValidator.callStatic.validate(
|
||||
'0xa68dd8cb83097765263adad881af6eed479c4a33', // WTF
|
||||
['0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'], // WETH
|
||||
wtfReserve.toString()
|
||||
)
|
||||
expect(isFot2).to.equal(Status.UNKN)
|
||||
})
|
||||
|
||||
it('succeeds to batch detect fot tokens', async () => {
|
||||
const isFots = await tokenValidator.callStatic.batchValidate(FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW)
|
||||
expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true
|
||||
})
|
||||
|
||||
it('succeeds to batch detect fot tokens when dont have pair with first base token', async () => {
|
||||
const isFots = await tokenValidator.callStatic.batchValidate(
|
||||
FOT_TOKENS,
|
||||
[testToken.address, ...BASE_TOKENS],
|
||||
AMOUNT_TO_BORROW
|
||||
)
|
||||
expect(isFots.every((isFot: Status) => isFot == Status.FOT)).to.be.true
|
||||
})
|
||||
|
||||
it('succeeds to detect non fot tokens', async () => {
|
||||
for (const token of NON_FOT_TOKENS) {
|
||||
const isFot = await tokenValidator.callStatic.validate(token, BASE_TOKENS, AMOUNT_TO_BORROW)
|
||||
expect(isFot).to.equal(Status.UNKN)
|
||||
}
|
||||
})
|
||||
|
||||
it('succeeds to batch detect non fot tokens', async () => {
|
||||
const isFots = await tokenValidator.callStatic.batchValidate(NON_FOT_TOKENS, BASE_TOKENS, AMOUNT_TO_BORROW)
|
||||
expect(isFots.every((isFot: Status) => isFot == Status.UNKN)).to.be.true
|
||||
})
|
||||
|
||||
it('succeeds to batch detect mix of fot tokens and non fot tokens', async () => {
|
||||
const isFots = await tokenValidator.callStatic.batchValidate(
|
||||
[NON_FOT_TOKENS[0], FOT_TOKENS[0], BROKEN_TOKENS[0]],
|
||||
BASE_TOKENS,
|
||||
1000
|
||||
)
|
||||
expect(isFots).to.deep.equal([Status.UNKN, Status.FOT, Status.STF])
|
||||
})
|
||||
|
||||
it('succeeds to return false if token doesnt have a pool with any of the base tokens', async () => {
|
||||
await tokenValidator.callStatic.validate(testToken.address, BASE_TOKENS, AMOUNT_TO_BORROW)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImmutableState bytecode size 1`] = `193`;
|
||||
@ -0,0 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 -> 1 1`] = `281505`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 0 tick, starting tick initialized 1`] = `123786`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 0 tick, starting tick not initialized 1`] = `104827`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 1 tick 1`] = `149094`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 2 tick 1`] = `186691`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 0 -> 2 cross 2 tick where after is initialized 1`] = `149132`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 0 tick, starting tick initialized 1`] = `97643`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 0 tick, starting tick not initialized 1`] = `97643`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 2 1`] = `179503`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 0 cross 2 where tick after is initialized 1`] = `179511`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInput V3 only 2 -> 1 1`] = `97318`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInputSingle V3 0 -> 2 1`] = `186673`;
|
||||
|
||||
exports[`MixedRouteQuoterV1 quotes #quoteExactInputSingle V3 2 -> 0 1`] = `179475`;
|
||||
@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Multicall gas cost of pay w/ multicall 1`] = `45928`;
|
||||
|
||||
exports[`Multicall gas cost of pay w/o multicall 1`] = `43364`;
|
||||
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Path gas cost 1`] = `451`;
|
||||
@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PoolAddress #computeAddress gas cost 1`] = `642`;
|
||||
|
||||
exports[`PoolAddress #computeAddress matches example from core repo 1`] = `"0x03D8bab195A5BC23d249693F53dfA0e358F2650D"`;
|
||||
@ -0,0 +1,51 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 -> 1 1`] = `277146`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 0 tick, starting tick initialized 1`] = `123797`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 0 tick, starting tick not initialized 1`] = `100962`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 1 tick 1`] = `144724`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 2 tick 1`] = `182321`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 0 -> 2 cross 2 tick where after is initialized 1`] = `144762`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 0 tick, starting tick initialized 1`] = `97654`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 0 tick, starting tick not initialized 1`] = `93779`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 2 1`] = `175133`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 2 -> 0 cross 2 where tick after is initialized 1`] = `175141`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInput 2 -> 1 1`] = `97329`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInputSingle 0 -> 2 1`] = `182303`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactInputSingle 2 -> 0 1`] = `175105`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 -> 1 1`] = `276746`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 0 tick starting tick initialized 1`] = `123352`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 0 tick starting tick not initialized 1`] = `100537`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 1 tick 1`] = `144010`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 2 tick 1`] = `181363`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 0 -> 2 cross 2 where tick after is initialized 1`] = `144048`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 1 tick 1`] = `137858`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 2 ticks 1`] = `175203`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 2 -> 0 cross 2 where tick after is initialized 1`] = `175197`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutput 2 -> 1 1`] = `97870`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutputSingle 0 -> 1 1`] = `105200`;
|
||||
|
||||
exports[`QuoterV2 quotes #quoteExactOutputSingle 1 -> 0 1`] = `98511`;
|
||||
@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 0 -> 1 -> 2 1`] = `175302`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 0 -> 1 1`] = `110495`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 0 -> 1 minimal 1`] = `98059`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 0 -> WETH9 1`] = `127401`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 2 trades (directly to sender) 1`] = `178981`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 2 trades (via router) 1`] = `188487`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput 3 trades (directly to sender) 1`] = `257705`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInput WETH9 -> 0 1`] = `108832`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInputSingle 0 -> 1 1`] = `109826`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInputSingle 0 -> WETH9 1`] = `126732`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactInputSingle WETH9 -> 0 1`] = `108163`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutput 0 -> 1 -> 2 1`] = `169268`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutput 0 -> 1 1`] = `111692`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutput 0 -> WETH9 1`] = `128610`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutput WETH9 -> 0 1`] = `119706`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutputSingle 0 -> 1 1`] = `111826`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutputSingle 0 -> WETH9 1`] = `128744`;
|
||||
|
||||
exports[`SwapRouter gas tests #exactOutputSingle WETH9 -> 0 1`] = `112627`;
|
||||
@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapRouter bytecode size 1`] = `24563`;
|
||||
156
lib/swap-router-contracts/test/contracts/WETH9.json
Normal file
156
lib/swap-router-contracts/test/contracts/WETH9.json
Normal file
File diff suppressed because one or more lines are too long
36
lib/swap-router-contracts/test/shared/completeFixture.ts
Normal file
36
lib/swap-router-contracts/test/shared/completeFixture.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { v3RouterFixture } from './externalFixtures'
|
||||
import { constants, Contract } from 'ethers'
|
||||
import { IWETH9, MockTimeSwapRouter02, TestERC20 } from '../../typechain'
|
||||
|
||||
const completeFixture: Fixture<{
|
||||
weth9: IWETH9
|
||||
factoryV2: Contract
|
||||
factory: Contract
|
||||
router: MockTimeSwapRouter02
|
||||
nft: Contract
|
||||
tokens: [TestERC20, TestERC20, TestERC20]
|
||||
}> = async ([wallet], provider) => {
|
||||
const { weth9, factoryV2, factory, nft, router } = await v3RouterFixture([wallet], provider)
|
||||
|
||||
const tokenFactory = await ethers.getContractFactory('TestERC20')
|
||||
const tokens: [TestERC20, TestERC20, TestERC20] = [
|
||||
(await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20, // do not use maxu256 to avoid overflowing
|
||||
(await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20,
|
||||
(await tokenFactory.deploy(constants.MaxUint256.div(2))) as TestERC20,
|
||||
]
|
||||
|
||||
tokens.sort((a, b) => (a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1))
|
||||
|
||||
return {
|
||||
weth9,
|
||||
factoryV2,
|
||||
factory,
|
||||
router,
|
||||
tokens,
|
||||
nft,
|
||||
}
|
||||
}
|
||||
|
||||
export default completeFixture
|
||||
22
lib/swap-router-contracts/test/shared/computePoolAddress.ts
Normal file
22
lib/swap-router-contracts/test/shared/computePoolAddress.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { bytecode } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json'
|
||||
import { utils } from 'ethers'
|
||||
|
||||
export const POOL_BYTECODE_HASH = utils.keccak256(bytecode)
|
||||
|
||||
export function computePoolAddress(factoryAddress: string, [tokenA, tokenB]: [string, string], fee: number): string {
|
||||
const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
const constructorArgumentsEncoded = utils.defaultAbiCoder.encode(
|
||||
['address', 'address', 'uint24'],
|
||||
[token0, token1, fee]
|
||||
)
|
||||
const create2Inputs = [
|
||||
'0xff',
|
||||
factoryAddress,
|
||||
// salt
|
||||
utils.keccak256(constructorArgumentsEncoded),
|
||||
// init code hash
|
||||
POOL_BYTECODE_HASH,
|
||||
]
|
||||
const sanitizedInputs = `0x${create2Inputs.map((i) => i.slice(2)).join('')}`
|
||||
return utils.getAddress(`0x${utils.keccak256(sanitizedInputs).slice(-40)}`)
|
||||
}
|
||||
21
lib/swap-router-contracts/test/shared/constants.ts
Normal file
21
lib/swap-router-contracts/test/shared/constants.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BigNumber } from 'ethers'
|
||||
|
||||
export const MaxUint128 = BigNumber.from(2).pow(128).sub(1)
|
||||
|
||||
export enum FeeAmount {
|
||||
LOW = 500,
|
||||
MEDIUM = 3000,
|
||||
HIGH = 10000,
|
||||
}
|
||||
|
||||
export const V2_FEE_PLACEHOLDER = 8388608 // 1 << 23
|
||||
|
||||
export const TICK_SPACINGS: { [amount in FeeAmount]: number } = {
|
||||
[FeeAmount.LOW]: 10,
|
||||
[FeeAmount.MEDIUM]: 60,
|
||||
[FeeAmount.HIGH]: 200,
|
||||
}
|
||||
|
||||
export const CONTRACT_BALANCE = 0
|
||||
export const MSG_SENDER = '0x0000000000000000000000000000000000000001'
|
||||
export const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'
|
||||
16
lib/swap-router-contracts/test/shared/encodePriceSqrt.ts
Normal file
16
lib/swap-router-contracts/test/shared/encodePriceSqrt.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import bn from 'bignumber.js'
|
||||
import { BigNumber, BigNumberish } from 'ethers'
|
||||
|
||||
bn.config({ EXPONENTIAL_AT: 999999, DECIMAL_PLACES: 40 })
|
||||
|
||||
// returns the sqrt price as a 64x96
|
||||
export function encodePriceSqrt(reserve1: BigNumberish, reserve0: BigNumberish): BigNumber {
|
||||
return BigNumber.from(
|
||||
new bn(reserve1.toString())
|
||||
.div(reserve0.toString())
|
||||
.sqrt()
|
||||
.multipliedBy(new bn(2).pow(96))
|
||||
.integerValue(3)
|
||||
.toString()
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { BigNumber } from 'ethers'
|
||||
|
||||
export function expandTo18Decimals(n: number): BigNumber {
|
||||
return BigNumber.from(n).mul(BigNumber.from(10).pow(18))
|
||||
}
|
||||
|
||||
export function expandToNDecimals(n: number, d: number): BigNumber {
|
||||
return BigNumber.from(n).mul(BigNumber.from(10).pow(d))
|
||||
}
|
||||
8
lib/swap-router-contracts/test/shared/expect.ts
Normal file
8
lib/swap-router-contracts/test/shared/expect.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect, use } from 'chai'
|
||||
import { solidity } from 'ethereum-waffle'
|
||||
import { jestSnapshotPlugin } from 'mocha-chai-jest-snapshot'
|
||||
|
||||
use(solidity)
|
||||
use(jestSnapshotPlugin())
|
||||
|
||||
export { expect }
|
||||
76
lib/swap-router-contracts/test/shared/externalFixtures.ts
Normal file
76
lib/swap-router-contracts/test/shared/externalFixtures.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import {
|
||||
abi as FACTORY_ABI,
|
||||
bytecode as FACTORY_BYTECODE,
|
||||
} from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
|
||||
import { abi as FACTORY_V2_ABI, bytecode as FACTORY_V2_BYTECODE } from '@uniswap/v2-core/build/UniswapV2Factory.json'
|
||||
import { Fixture } from 'ethereum-waffle'
|
||||
import { ethers, waffle } from 'hardhat'
|
||||
import { IWETH9, MockTimeSwapRouter02 } from '../../typechain'
|
||||
|
||||
import WETH9 from '../contracts/WETH9.json'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { constants } from 'ethers'
|
||||
|
||||
import {
|
||||
abi as NFT_POSITION_MANAGER_ABI,
|
||||
bytecode as NFT_POSITION_MANAGER_BYTECODE,
|
||||
} from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
|
||||
const wethFixture: Fixture<{ weth9: IWETH9 }> = async ([wallet]) => {
|
||||
const weth9 = (await waffle.deployContract(wallet, {
|
||||
bytecode: WETH9.bytecode,
|
||||
abi: WETH9.abi,
|
||||
})) as IWETH9
|
||||
|
||||
return { weth9 }
|
||||
}
|
||||
|
||||
export const v2FactoryFixture: Fixture<{ factory: Contract }> = async ([wallet]) => {
|
||||
const factory = await waffle.deployContract(
|
||||
wallet,
|
||||
{
|
||||
bytecode: FACTORY_V2_BYTECODE,
|
||||
abi: FACTORY_V2_ABI,
|
||||
},
|
||||
[constants.AddressZero]
|
||||
)
|
||||
|
||||
return { factory }
|
||||
}
|
||||
|
||||
const v3CoreFactoryFixture: Fixture<Contract> = async ([wallet]) => {
|
||||
return await waffle.deployContract(wallet, {
|
||||
bytecode: FACTORY_BYTECODE,
|
||||
abi: FACTORY_ABI,
|
||||
})
|
||||
}
|
||||
|
||||
export const v3RouterFixture: Fixture<{
|
||||
weth9: IWETH9
|
||||
factoryV2: Contract
|
||||
factory: Contract
|
||||
nft: Contract
|
||||
router: MockTimeSwapRouter02
|
||||
}> = async ([wallet], provider) => {
|
||||
const { weth9 } = await wethFixture([wallet], provider)
|
||||
const { factory: factoryV2 } = await v2FactoryFixture([wallet], provider)
|
||||
const factory = await v3CoreFactoryFixture([wallet], provider)
|
||||
|
||||
const nft = await waffle.deployContract(
|
||||
wallet,
|
||||
{
|
||||
bytecode: NFT_POSITION_MANAGER_BYTECODE,
|
||||
abi: NFT_POSITION_MANAGER_ABI,
|
||||
},
|
||||
[factory.address, weth9.address, constants.AddressZero]
|
||||
)
|
||||
|
||||
const router = (await (await ethers.getContractFactory('MockTimeSwapRouter02')).deploy(
|
||||
factoryV2.address,
|
||||
factory.address,
|
||||
nft.address,
|
||||
weth9.address
|
||||
)) as MockTimeSwapRouter02
|
||||
|
||||
return { weth9, factoryV2, factory, nft, router }
|
||||
}
|
||||
61
lib/swap-router-contracts/test/shared/path.ts
Normal file
61
lib/swap-router-contracts/test/shared/path.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { utils } from 'ethers'
|
||||
import { FeeAmount } from './constants'
|
||||
|
||||
const ADDR_SIZE = 20
|
||||
const FEE_SIZE = 3
|
||||
const OFFSET = ADDR_SIZE + FEE_SIZE
|
||||
const DATA_SIZE = OFFSET + ADDR_SIZE
|
||||
|
||||
export function encodePath(path: string[], fees: FeeAmount[]): string {
|
||||
if (path.length != fees.length + 1) {
|
||||
throw new Error('path/fee lengths do not match')
|
||||
}
|
||||
|
||||
let encoded = '0x'
|
||||
for (let i = 0; i < fees.length; i++) {
|
||||
// 20 byte encoding of the address
|
||||
encoded += path[i].slice(2)
|
||||
// 3 byte encoding of the fee
|
||||
encoded += fees[i].toString(16).padStart(2 * FEE_SIZE, '0')
|
||||
}
|
||||
// encode the final token
|
||||
encoded += path[path.length - 1].slice(2)
|
||||
|
||||
return encoded.toLowerCase()
|
||||
}
|
||||
|
||||
function decodeOne(tokenFeeToken: Buffer): [[string, string], number] {
|
||||
// reads the first 20 bytes for the token address
|
||||
const tokenABuf = tokenFeeToken.slice(0, ADDR_SIZE)
|
||||
const tokenA = utils.getAddress('0x' + tokenABuf.toString('hex'))
|
||||
|
||||
// reads the next 2 bytes for the fee
|
||||
const feeBuf = tokenFeeToken.slice(ADDR_SIZE, OFFSET)
|
||||
const fee = feeBuf.readUIntBE(0, FEE_SIZE)
|
||||
|
||||
// reads the next 20 bytes for the token address
|
||||
const tokenBBuf = tokenFeeToken.slice(OFFSET, DATA_SIZE)
|
||||
const tokenB = utils.getAddress('0x' + tokenBBuf.toString('hex'))
|
||||
|
||||
return [[tokenA, tokenB], fee]
|
||||
}
|
||||
|
||||
export function decodePath(path: string): [string[], number[]] {
|
||||
let data = Buffer.from(path.slice(2), 'hex')
|
||||
|
||||
let tokens: string[] = []
|
||||
let fees: number[] = []
|
||||
let i = 0
|
||||
let finalToken: string = ''
|
||||
while (data.length >= DATA_SIZE) {
|
||||
const [[tokenA, tokenB], fee] = decodeOne(data)
|
||||
finalToken = tokenB
|
||||
tokens = [...tokens, tokenA]
|
||||
fees = [...fees, fee]
|
||||
data = data.slice((i + 1) * OFFSET)
|
||||
i += 1
|
||||
}
|
||||
tokens = [...tokens, finalToken]
|
||||
|
||||
return [tokens, fees]
|
||||
}
|
||||
160
lib/swap-router-contracts/test/shared/quoter.ts
Normal file
160
lib/swap-router-contracts/test/shared/quoter.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { Wallet, Contract } from 'ethers'
|
||||
import { FeeAmount, TICK_SPACINGS } from './constants'
|
||||
import { encodePriceSqrt } from './encodePriceSqrt'
|
||||
import { getMaxTick, getMinTick } from './ticks'
|
||||
|
||||
export async function createPool(nft: Contract, wallet: Wallet, tokenAddressA: string, tokenAddressB: string) {
|
||||
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
|
||||
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
|
||||
|
||||
await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1))
|
||||
|
||||
const liquidityParams = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 1000000,
|
||||
amount1Desired: 1000000,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
return nft.mint(liquidityParams)
|
||||
}
|
||||
|
||||
export async function createPoolWithMultiplePositions(
|
||||
nft: Contract,
|
||||
wallet: Wallet,
|
||||
tokenAddressA: string,
|
||||
tokenAddressB: string
|
||||
) {
|
||||
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
|
||||
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
|
||||
|
||||
await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1))
|
||||
|
||||
const liquidityParams = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 1000000,
|
||||
amount1Desired: 1000000,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
await nft.mint(liquidityParams)
|
||||
|
||||
const liquidityParams2 = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -60,
|
||||
tickUpper: 60,
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 100,
|
||||
amount1Desired: 100,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
await nft.mint(liquidityParams2)
|
||||
|
||||
const liquidityParams3 = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -120,
|
||||
tickUpper: 120,
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 100,
|
||||
amount1Desired: 100,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
return nft.mint(liquidityParams3)
|
||||
}
|
||||
|
||||
export async function createPoolWithZeroTickInitialized(
|
||||
nft: Contract,
|
||||
wallet: Wallet,
|
||||
tokenAddressA: string,
|
||||
tokenAddressB: string
|
||||
) {
|
||||
if (tokenAddressA.toLowerCase() > tokenAddressB.toLowerCase())
|
||||
[tokenAddressA, tokenAddressB] = [tokenAddressB, tokenAddressA]
|
||||
|
||||
await nft.createAndInitializePoolIfNecessary(tokenAddressA, tokenAddressB, FeeAmount.MEDIUM, encodePriceSqrt(1, 1))
|
||||
|
||||
const liquidityParams = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: getMinTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
tickUpper: getMaxTick(TICK_SPACINGS[FeeAmount.MEDIUM]),
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 1000000,
|
||||
amount1Desired: 1000000,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
await nft.mint(liquidityParams)
|
||||
|
||||
const liquidityParams2 = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: 0,
|
||||
tickUpper: 60,
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 100,
|
||||
amount1Desired: 100,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
await nft.mint(liquidityParams2)
|
||||
|
||||
const liquidityParams3 = {
|
||||
token0: tokenAddressA,
|
||||
token1: tokenAddressB,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -120,
|
||||
tickUpper: 0,
|
||||
recipient: wallet.address,
|
||||
amount0Desired: 100,
|
||||
amount1Desired: 100,
|
||||
amount0Min: 0,
|
||||
amount1Min: 0,
|
||||
deadline: 2 ** 32,
|
||||
}
|
||||
|
||||
return nft.mint(liquidityParams3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create V2 pairs for testing with IL routes
|
||||
*/
|
||||
export async function createPair(v2Factory: Contract, tokenAddressA: string, tokenAddressB: string): Promise<string> {
|
||||
// .createPair() sorts the tokens already
|
||||
const receipt = await (await v2Factory.createPair(tokenAddressA, tokenAddressB)).wait()
|
||||
// we can extract the pair address from the emitted event
|
||||
// always the 3rd element: emit PairCreated(token0, token1, pair, allPairs.length);
|
||||
const pairAddress = receipt.events[0].args[2]
|
||||
if (!pairAddress) throw new Error('pairAddress not found in txn receipt')
|
||||
return pairAddress
|
||||
}
|
||||
27
lib/swap-router-contracts/test/shared/snapshotGasCost.ts
Normal file
27
lib/swap-router-contracts/test/shared/snapshotGasCost.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TransactionReceipt, TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { expect } from './expect'
|
||||
import { Contract, BigNumber, ContractTransaction } from 'ethers'
|
||||
|
||||
export default async function snapshotGasCost(
|
||||
x:
|
||||
| TransactionResponse
|
||||
| Promise<TransactionResponse>
|
||||
| ContractTransaction
|
||||
| Promise<ContractTransaction>
|
||||
| TransactionReceipt
|
||||
| Promise<BigNumber>
|
||||
| BigNumber
|
||||
| Contract
|
||||
| Promise<Contract>
|
||||
): Promise<void> {
|
||||
const resolved = await x
|
||||
if ('deployTransaction' in resolved) {
|
||||
const receipt = await resolved.deployTransaction.wait()
|
||||
expect(receipt.gasUsed.toNumber()).toMatchSnapshot()
|
||||
} else if ('wait' in resolved) {
|
||||
const waited = await resolved.wait()
|
||||
expect(waited.gasUsed.toNumber()).toMatchSnapshot()
|
||||
} else if (BigNumber.isBigNumber(resolved)) {
|
||||
expect(resolved.toNumber()).toMatchSnapshot()
|
||||
}
|
||||
}
|
||||
2
lib/swap-router-contracts/test/shared/ticks.ts
Normal file
2
lib/swap-router-contracts/test/shared/ticks.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const getMinTick = (tickSpacing: number) => Math.ceil(-887272 / tickSpacing) * tickSpacing
|
||||
export const getMaxTick = (tickSpacing: number) => Math.floor(887272 / tickSpacing) * tickSpacing
|
||||
14
lib/swap-router-contracts/tsconfig.json
Normal file
14
lib/swap-router-contracts/tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"typeRoots": ["./typechain", "./node_modules/@types"],
|
||||
"types": ["@nomiclabs/hardhat-ethers", "@nomiclabs/hardhat-waffle"]
|
||||
},
|
||||
"include": ["./test"],
|
||||
"files": ["./hardhat.config.ts"]
|
||||
}
|
||||
8128
lib/swap-router-contracts/yarn.lock
Normal file
8128
lib/swap-router-contracts/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user