├── .gitattributes ├── .github └── workflows │ ├── factory.yaml │ ├── gauge.yaml │ ├── lint.yaml │ ├── metapool.yaml │ ├── migrator.yaml │ ├── owner-proxy.yaml │ ├── swaps.yaml │ ├── token.yaml │ └── zap.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── brownie-config.yaml ├── brownie_hooks.py ├── contracts ├── Factory.vy ├── FactorySidechains.vy ├── LiquidityGauge.vy ├── ManagerProxy.vy ├── OwnerProxy.vy ├── PoolMigrator.vy ├── implementations │ ├── meta │ │ ├── MetaBTC.vy │ │ ├── MetaBTCBalances.vy │ │ ├── MetaUSD.vy │ │ └── MetaUSDBalances.vy │ ├── plain-2 │ │ ├── Plain2Balances.vy │ │ ├── Plain2Basic.vy │ │ ├── Plain2BasicEMA.vy │ │ ├── Plain2ETH.vy │ │ ├── Plain2ETHEMA.vy │ │ ├── Plain2Optimized.vy │ │ └── Plain2Price.vy │ ├── plain-3 │ │ ├── Plain3Balances.vy │ │ ├── Plain3Basic.vy │ │ ├── Plain3ETH.vy │ │ ├── Plain3Optimized.vy │ │ └── Plain3Price.vy │ ├── plain-4 │ │ ├── Plain4Balances.vy │ │ ├── Plain4Basic.vy │ │ ├── Plain4ETH.vy │ │ ├── Plain4Optimized.vy │ │ └── Plain4Price.vy │ └── templates │ │ ├── DepositZap.vy │ │ ├── MetaBalances.vy │ │ └── MetaStandard.vy ├── testing │ ├── AaveLendingPoolMock.sol │ ├── AddressProvider.vy │ ├── CurvePool.vy │ ├── CurveTokenV3.vy │ ├── ERC20Mock.vy │ ├── MockBoostDelegationProxy.vy │ ├── Registry.vy │ ├── aERC20.sol │ └── rERC20.vy └── zaps │ ├── DepositZapBTC.vy │ └── DepositZapUSD.vy ├── package.json ├── pyproject.toml ├── requirements.txt ├── scripts ├── deploy.py └── deploy_templates.py ├── setup.cfg └── tests ├── conftest.py ├── fixtures ├── __init__.py ├── accounts.py ├── coins.py ├── constants.py ├── deployments.py └── functions.py ├── gauge ├── test_add_reward.py ├── test_approve.py ├── test_boost_delegation.py ├── test_checkpoint.py ├── test_claim_rewards_multiple.py ├── test_claim_rewards_none.py ├── test_claim_rewards_transfer.py ├── test_claim_rewards_unipool.py ├── test_deposit_for.py ├── test_deposit_withdraw.py ├── test_kick.py ├── test_mass_exit_reward_claim.py ├── test_set_rewards_receiver.py ├── test_transfer.py └── test_transferFrom.py ├── pools ├── common │ ├── test_add_liquidity.py │ ├── test_add_liquidity_initial.py │ ├── test_claim_fees.py │ ├── test_exchange.py │ ├── test_exchange_imbalanced.py │ ├── test_exchange_reverts.py │ ├── test_get_virtual_price.py │ ├── test_ramp_A_precise.py │ ├── test_receiver.py │ ├── test_remove_liquidity.py │ ├── test_remove_liquidity_imbalance.py │ ├── test_remove_liquidity_one_coin.py │ └── test_withdraw_admin_fees.py ├── meta │ ├── test_exchange_meta.py │ ├── test_exchange_underlying.py │ ├── test_exchange_underlying_reverts.py │ ├── test_get_virtual_price_meta.py │ └── test_receiver_meta.py └── rebase │ └── test_token_balances.py ├── test_factory.py ├── test_gauge_manager_proxy.py ├── test_migrator.py ├── test_owner_proxy.py ├── token ├── test_token_approve.py ├── test_token_transfer.py └── test_token_transferFrom.py └── zaps ├── test_add_liquidity_initial_zap.py ├── test_add_liquidity_zap.py ├── test_receiver_zap.py ├── test_remove_liquidity_imbalance_zap.py ├── test_remove_liquidity_one_coin_zap.py ├── test_remove_liquidity_zap.py └── test_return_values.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /.github/workflows/factory.yaml: -------------------------------------------------------------------------------- 1 | name: factory 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'tests/test_factory.py' 7 | - 'contracts/Factory.vy' 8 | push: 9 | paths: 10 | - 'tests/test_factory.py' 11 | - 'contracts/Factory.vy' 12 | 13 | env: 14 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 17 | NODE_OPTIONS: --max_old_space_size=4096 18 | 19 | jobs: 20 | 21 | unitary: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | pool_type: ["basic", "eth", "optimized", "rebase", "meta-usd"] 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Cache Compiler Installations 32 | uses: actions/cache@v2 33 | with: 34 | path: | 35 | ~/.solcx 36 | ~/.vvm 37 | ~/.brownie 38 | /home/runner/work/curve-factory/build 39 | key: compiler-cache 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v1 43 | 44 | - name: Install Ganache 45 | run: npm install 46 | 47 | - name: Setup Python 3.8 48 | uses: actions/setup-python@v2 49 | with: 50 | python-version: 3.8 51 | 52 | - name: Install Requirements 53 | run: pip install -r requirements.txt 54 | 55 | - name: Run Tests 56 | run: brownie test tests/test_factory.py --cache-clear --pool-type ${{ matrix.pool_type }} -x 57 | -------------------------------------------------------------------------------- /.github/workflows/gauge.yaml: -------------------------------------------------------------------------------- 1 | name: gauge 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "tests/gauge/*.py" 7 | - "contracts/LiquidityGauge.vy" 8 | push: 9 | paths: 10 | - "tests/gauge/*.py" 11 | - "contracts/LiquidityGauge.vy" 12 | 13 | env: 14 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 17 | NODE_OPTIONS: --max_old_space_size=4096 18 | 19 | jobs: 20 | unitary: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Cache Compiler Installations 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ~/.solcx 31 | ~/.vvm 32 | ~/.brownie 33 | /home/runner/work/curve-factory/build 34 | key: compiler-cache 35 | 36 | - name: Setup Node.js 37 | uses: actions/setup-node@v1 38 | 39 | - name: Install Ganache 40 | run: npm install 41 | 42 | - name: Setup Python 3.8 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: 3.8 46 | 47 | - name: Install Requirements 48 | run: pip install -r requirements.txt 49 | 50 | - name: Run Tests 51 | run: brownie test tests/gauge/ --decimals 18 --return-type revert --pool-type basic --plain-pool-size 2 --cache-clear 52 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: linting 4 | 5 | jobs: 6 | 7 | lint: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Python 3.8 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | 18 | - name: Install Requirements 19 | run: pip install -r requirements.txt 20 | 21 | - name: Run black 22 | run: black scripts tests 23 | 24 | - name: Run flake8 25 | run: flake8 scripts tests 26 | 27 | - name: Run isort 28 | run: isort scripts tests 29 | -------------------------------------------------------------------------------- /.github/workflows/metapool.yaml: -------------------------------------------------------------------------------- 1 | name: metapool 2 | 3 | on: ["push", "pull_request"] 4 | 5 | env: 6 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 9 | NODE_OPTIONS: --max_old_space_size=4096 10 | 11 | jobs: 12 | 13 | unitary: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | decimals: [18] 19 | return_type: ["revert"] 20 | plain_pool_type: ["meta-usd", "meta-btc", "meta-side"] 21 | pool_size: [2] 22 | 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache Compiler Installations 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.solcx 32 | ~/.vvm 33 | ~/.brownie 34 | /home/runner/work/curve-factory/build 35 | key: compiler-cache 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v1 39 | 40 | - name: Install Ganache 41 | run: npm install 42 | 43 | - name: Setup Python 3.8 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: 3.8 47 | 48 | - name: Install Requirements 49 | run: pip install -r requirements.txt 50 | 51 | - name: Run Tests 52 | run: brownie test tests/pools --decimals ${{ matrix.decimals }} --return-type ${{ matrix.return_type }} --pool-type ${{ matrix.plain_pool_type }} --plain-pool-size ${{ matrix.pool_size }} --cache-clear 53 | -------------------------------------------------------------------------------- /.github/workflows/migrator.yaml: -------------------------------------------------------------------------------- 1 | name: migrator 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'tests/test_mitrator.py' 7 | - 'contracts/PoolMigrator.vy' 8 | push: 9 | paths: 10 | - 'tests/test_migrator.py' 11 | - 'contracts/PoolMigrator.vy' 12 | 13 | env: 14 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 17 | NODE_OPTIONS: --max_old_space_size=4096 18 | 19 | jobs: 20 | 21 | unitary: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache Compiler Installations 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.solcx 32 | ~/.vvm 33 | ~/.brownie 34 | /home/runner/work/curve-factory/build 35 | key: compiler-cache 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v1 39 | 40 | - name: Install Ganache 41 | run: npm install 42 | 43 | - name: Setup Python 3.8 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: 3.8 47 | 48 | - name: Install Requirements 49 | run: pip install -r requirements.txt 50 | 51 | - name: Run Tests 52 | run: brownie test tests/test_migrator.py 53 | -------------------------------------------------------------------------------- /.github/workflows/owner-proxy.yaml: -------------------------------------------------------------------------------- 1 | name: owner-proxy 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'tests/test_owner_proxy.py' 7 | - 'contracts/OwnerProxy.vy' 8 | push: 9 | paths: 10 | - 'tests/test_owner_proxy.py' 11 | - 'contracts/OwnerProxy.vy' 12 | 13 | env: 14 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 17 | NODE_OPTIONS: --max_old_space_size=4096 18 | 19 | jobs: 20 | 21 | unitary: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache Compiler Installations 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.solcx 32 | ~/.vvm 33 | ~/.brownie 34 | /home/runner/work/curve-factory/build 35 | key: compiler-cache 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v1 39 | 40 | - name: Install Ganache 41 | run: npm install 42 | 43 | - name: Setup Python 3.8 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: 3.8 47 | 48 | - name: Install Requirements 49 | run: pip install -r requirements.txt 50 | 51 | - name: Run Tests 52 | run: brownie test tests/test_owner_proxy.py 53 | -------------------------------------------------------------------------------- /.github/workflows/swaps.yaml: -------------------------------------------------------------------------------- 1 | name: swaps 2 | 3 | on: ["push", "pull_request"] 4 | 5 | env: 6 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 9 | NODE_OPTIONS: --max_old_space_size=4096 10 | 11 | jobs: 12 | 13 | unitary: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | decimals: [18] 19 | return_type: ["revert"] 20 | plain_pool_type: ["basic", "eth", "optimized", "rebase"] 21 | pool_size: [2, 3, 4] 22 | 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache Compiler Installations 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.solcx 32 | ~/.vvm 33 | ~/.brownie 34 | /home/runner/work/curve-factory/build 35 | key: compiler-cache 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v1 39 | 40 | - name: Install Ganache 41 | run: npm install 42 | 43 | - name: Setup Python 3.8 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: 3.8 47 | 48 | - name: Install Requirements 49 | run: pip install -r requirements.txt 50 | 51 | - name: Run Tests 52 | run: brownie test tests/pools --decimals ${{ matrix.decimals }} --return-type ${{ matrix.return_type }} --pool-type ${{ matrix.plain_pool_type }} --plain-pool-size ${{ matrix.pool_size }} --cache-clear 53 | -------------------------------------------------------------------------------- /.github/workflows/token.yaml: -------------------------------------------------------------------------------- 1 | name: token 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'tests/token/**/*.py' 7 | - 'contracts/implementations/*.vy' 8 | push: 9 | paths: 10 | - 'tests/token/**/*.py' 11 | - 'contracts/implementations/*.vy' 12 | 13 | env: 14 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 17 | NODE_OPTIONS: --max_old_space_size=4096 18 | 19 | jobs: 20 | 21 | unitary: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Cache Compiler Installations 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ~/.solcx 32 | ~/.vvm 33 | ~/.brownie 34 | /home/runner/work/curve-factory/build 35 | key: compiler-cache 36 | 37 | - name: Setup Node.js 38 | uses: actions/setup-node@v1 39 | 40 | - name: Install Ganache 41 | run: npm install 42 | 43 | - name: Setup Python 3.8 44 | uses: actions/setup-python@v2 45 | with: 46 | python-version: 3.8 47 | 48 | - name: Install Requirements 49 | run: pip install -r requirements.txt 50 | 51 | - name: Run Tests 52 | run: brownie test tests/token 53 | -------------------------------------------------------------------------------- /.github/workflows/zap.yaml: -------------------------------------------------------------------------------- 1 | name: zap 2 | 3 | on: ["push", "pull_request"] 4 | 5 | env: 6 | ETHERSCAN_TOKEN: 9MKURTHE8FNA9NRUUJBHMUEVY6IQ5K1EGY 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | WEB3_INFURA_PROJECT_ID: 4b7217c6901c42f2bd9e8509baa0699d 9 | NODE_OPTIONS: --max_old_space_size=4096 10 | 11 | jobs: 12 | 13 | unitary: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Cache Compiler Installations 20 | uses: actions/cache@v2 21 | with: 22 | path: | 23 | ~/.solcx 24 | ~/.vvm 25 | ~/.brownie 26 | /home/runner/work/curve-factory/build 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test tests/zaps/ --pool-type meta-side --cache-clear 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | node_modules 7 | .build/ 8 | .idea 9 | 10 | ### Python ### 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | pytestdebug.log 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | doc/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | pythonenv* 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | # pytype static type analyzer 145 | .pytype/ 146 | 147 | # profiling data 148 | .prof 149 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 21.6b0 4 | hooks: 5 | - id: black 6 | - repo: https://gitlab.com/pycqa/flake8 7 | rev: 3.8.4 8 | hooks: 9 | - id: flake8 10 | - repo: https://github.com/PyCQA/isort 11 | rev: 5.7.0 12 | hooks: 13 | - id: isort 14 | 15 | default_language_version: 16 | python: python3.8 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Curve.Fi, 2021 2 | 3 | Except as otherwise provided in the header of a specific source code 4 | file in this repository: (a) all intellectual property (including all 5 | source code, designs and protocols) contained in this repository has been 6 | published for informational purposes only; (b) no license, right of 7 | reproduction or distribution or other right with respect thereto is 8 | granted or implied; and (c) all moral, intellectual property and other 9 | rights are hereby reserved by the copyright holder. 10 | 11 | THE SOFTWARE AND INTELLECTUAL PROPERTY INCLUDED IN THIS REPOSITORY 12 | IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS," AND ANY EXPRESS 13 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR 16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE OR 18 | INTELLECTUAL PROPERTY (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 19 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 20 | BUSINESS INTERRUPTION), HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, 21 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF 22 | SUCH DAMAGES WERE REASONABLY FORESEEABLE OR THE COPYRIGHT HOLDERS WERE 23 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # curve-factory 2 | 3 | Permissionless deployment of Curve metapools. 4 | 5 | ## Overview 6 | 7 | The metapool factory has several core components: 8 | 9 | * [`Factory`](contracts/Factory.vy) is the main contract used to deploy new metapools. It also acts a registry for finding the deployed pools and querying information about them. 10 | * New pools are deployed via a proxy contract. The [implementation contract](contracts/MetaImplementationUSD.vy) targetted by the proxy is determined according to the base pool. This is the same technique used to create pools in Uniswap V1. 11 | * [Deposit contracts](contracts/DepositZapUSD.vy) ("zaps") are used for wrapping and unwrapping underlying assets when depositing into or withdrawing from pools. 12 | 13 | See the [documentation](https://curve.readthedocs.io/factory-overview.html) for more detailed information. 14 | 15 | ### Dependencies 16 | 17 | * [python3](https://www.python.org/downloads/release/python-368/) version 3.6 or greater, python3-dev 18 | * [brownie](https://github.com/eth-brownie/brownie) - tested with version [1.15.0](https://github.com/eth-brownie/brownie/releases/tag/v1.15.0) 19 | * [brownie-token-tester](https://github.com/iamdefinitelyahuman/brownie-token-tester) 20 | * [ganache-cli](https://github.com/trufflesuite/ganache-cli) - tested with version [6.12.1](https://github.com/trufflesuite/ganache-cli/releases/tag/v6.12.1) 21 | 22 | ### Testing 23 | 24 | Testing is performed in a forked mainnet environment. 25 | 26 | To run the unit tests: 27 | 28 | ```bash 29 | brownie test 30 | ``` 31 | 32 | You can optionally include the following flags: 33 | 34 | * `--decimals`: The number of decimal places for the token used to test the factory pool. Default is 18. 35 | * `--return_value`: The return value given by the token used to test the factory pool. Valid options are `True` and `None`. 36 | 37 | ### Deployment 38 | 39 | To deploy the contracts, first modify the [`deployment script`](scripts/deploy.py) to unlock the account you wish to deploy from. Then: 40 | 41 | ```bash 42 | brownie run deploy --network mainnet 43 | ``` 44 | 45 | ### License 46 | 47 | (c) Curve.Fi, 2020 - [All rights reserved](LICENSE). 48 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | 3 | networks: 4 | development: 5 | cmd_settings: 6 | default_balance: 1_000_000_000 7 | chain_id: 1337 8 | 9 | dependencies: curvefi/curve-dao-contracts@1.3.0 10 | -------------------------------------------------------------------------------- /brownie_hooks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Compile-time hook used to set constants. 3 | """ 4 | from pathlib import Path 5 | 6 | from brownie import ZERO_ADDRESS 7 | from brownie._config import CONFIG 8 | 9 | 10 | def brownie_load_source(path: Path, source: str): 11 | 12 | if "templates" not in path.parts: 13 | # compile-time substitution only applies to pool templates 14 | # and only when in test mode 15 | return source 16 | 17 | replacements = { 18 | "___BASE_N_COINS___": 3, 19 | "___BASE_COINS___": f"[{', '.join([ZERO_ADDRESS] * 3)}]", 20 | } 21 | if CONFIG.mode != "test": 22 | replacements = { 23 | "___BASE_N_COINS___": 69, 24 | "___BASE_COINS___": f"[{', '.join([ZERO_ADDRESS] * 69)}]", 25 | } 26 | 27 | for k, v in replacements.items(): 28 | source = source.replace(k, str(v)) 29 | 30 | return source 31 | -------------------------------------------------------------------------------- /contracts/ManagerProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | """ 3 | @notice Gauge Manager Proxy 4 | @author CurveFi 5 | """ 6 | 7 | 8 | interface Factory: 9 | def admin() -> address: view 10 | def deploy_gauge(_pool: address) -> address: nonpayable 11 | 12 | interface OwnerProxy: 13 | def add_reward(_gauge: address, _reward_token: address, _distributor: address): nonpayable 14 | def ownership_admin() -> address: view 15 | def set_reward_distributor(_gauge: address, _reward_token: address, _distributor: address): nonpayable 16 | 17 | 18 | event SetManager: 19 | _manager: indexed(address) 20 | 21 | event SetGaugeManager: 22 | _gauge: indexed(address) 23 | _gauge_manager: indexed(address) 24 | 25 | 26 | FACTORY: immutable(address) 27 | OWNER_PROXY: immutable(address) 28 | 29 | 30 | gauge_manager: public(HashMap[address, address]) 31 | manager: public(address) 32 | 33 | 34 | @external 35 | def __init__(_factory: address, _manager: address): 36 | FACTORY = _factory 37 | OWNER_PROXY = Factory(_factory).admin() 38 | 39 | self.manager = _manager 40 | log SetManager(_manager) 41 | 42 | 43 | @external 44 | def add_reward(_gauge: address, _reward_token: address, _distributor: address): 45 | """ 46 | @notice Add a reward to a gauge 47 | @param _gauge The gauge the reward will be added to 48 | @param _reward_token The token to be added as a reward (should be ERC20-compliant) 49 | @param _distributor The account which will top-up, and distribute the rewards. 50 | """ 51 | assert msg.sender in [self.gauge_manager[_gauge], self.manager] 52 | 53 | OwnerProxy(OWNER_PROXY).add_reward(_gauge, _reward_token, _distributor) 54 | 55 | 56 | @external 57 | def set_reward_distributor(_gauge: address, _reward_token: address, _distributor: address): 58 | """ 59 | @notice Set the reward distributor for a gauge 60 | @param _gauge The gauge to update 61 | @param _reward_token The reward token for which the distributor will be changed 62 | @param _distributor The new distributor for the reward token. 63 | """ 64 | assert msg.sender in [self.gauge_manager[_gauge], self.manager] 65 | 66 | OwnerProxy(OWNER_PROXY).set_reward_distributor(_gauge, _reward_token, _distributor) 67 | 68 | 69 | @external 70 | def deploy_gauge(_pool: address, _gauge_manager: address = msg.sender) -> address: 71 | """ 72 | @notice Deploy a gauge, and set _gauge_manager as the manager 73 | @param _pool The pool to deploy a gauge for 74 | @param _gauge_manager The account to which will manage rewards for the gauge 75 | """ 76 | gauge: address = Factory(FACTORY).deploy_gauge(_pool) 77 | 78 | self.gauge_manager[gauge] = _gauge_manager 79 | log SetGaugeManager(gauge, _gauge_manager) 80 | return gauge 81 | 82 | 83 | @external 84 | def set_gauge_manager(_gauge: address, _gauge_manager: address): 85 | """ 86 | @notice Change the gauge manager for a gauge 87 | @dev The manager of this contract, or the ownership admin can outright modify gauge 88 | managership. A gauge manager can also transfer managership to a new manager via this 89 | method, but only for the gauge which they are the manager of. 90 | @param _gauge The gauge to change the managership of 91 | @param _gauge_manager The account to set as the new manager of the gauge. 92 | """ 93 | if msg.sender not in [self.manager, OwnerProxy(OWNER_PROXY).ownership_admin()]: 94 | assert msg.sender == self.gauge_manager[_gauge] 95 | 96 | self.gauge_manager[_gauge] = _gauge_manager 97 | log SetGaugeManager(_gauge, _gauge_manager) 98 | 99 | 100 | @external 101 | def set_manager(_manager: address): 102 | """ 103 | @notice Set the manager of this contract 104 | @param _manager The account to set as the manager 105 | """ 106 | assert msg.sender in [self.manager, OwnerProxy(OWNER_PROXY).ownership_admin()] 107 | 108 | self.manager = _manager 109 | log SetManager(_manager) 110 | 111 | 112 | @pure 113 | @external 114 | def factory() -> address: 115 | return FACTORY 116 | 117 | 118 | @pure 119 | @external 120 | def owner_proxy() -> address: 121 | return OWNER_PROXY -------------------------------------------------------------------------------- /contracts/OwnerProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | """ 3 | @title Curve StableSwap Owner Proxy 4 | @author Curve Finance 5 | @license MIT 6 | @notice Allows DAO ownership of `Factory` and it's deployed pools 7 | """ 8 | 9 | interface ManagerProxy: 10 | def gauge_manager(_gauge: address) -> address: view 11 | 12 | interface Curve: 13 | def ramp_A(_future_A: uint256, _future_time: uint256): nonpayable 14 | def stop_ramp_A(): nonpayable 15 | def set_ma_exp_time(_ma_exp_time: uint256): nonpayable 16 | def ma_exp_time() -> uint256: view 17 | def commit_new_fee(_new_fee: uint256): nonpayable 18 | def apply_new_fee(): nonpayable 19 | 20 | interface Gauge: 21 | def set_killed(_is_killed: bool): nonpayable 22 | def add_reward(_reward_token: address, _distributor: address): nonpayable 23 | def set_reward_distributor(_reward_token: address, _distributor: address): nonpayable 24 | 25 | interface Factory: 26 | def add_base_pool( 27 | _base_pool: address, 28 | _fee_receiver: address, 29 | _asset_type: uint256, 30 | _implementations: address[10], 31 | ): nonpayable 32 | def set_metapool_implementations( 33 | _base_pool: address, 34 | _implementations: address[10], 35 | ): nonpayable 36 | def set_plain_implementations( 37 | _n_coins: uint256, 38 | _implementations: address[10], 39 | ): nonpayable 40 | def set_gauge_implementation(_gauge_implementation: address): nonpayable 41 | def set_fee_receiver(_base_pool: address, _fee_receiver: address): nonpayable 42 | def commit_transfer_ownership(addr: address): nonpayable 43 | def accept_transfer_ownership(): nonpayable 44 | def set_manager(_manager: address): nonpayable 45 | def deploy_plain_pool( 46 | _name: String[32], 47 | _symbol: String[10], 48 | _coins: address[4], 49 | _A: uint256, 50 | _fee: uint256, 51 | _asset_type: uint256, 52 | _implementation_idx: uint256, 53 | ) -> address: nonpayable 54 | def deploy_gauge(_pool: address) -> address: nonpayable 55 | 56 | 57 | event CommitAdmins: 58 | ownership_admin: address 59 | parameter_admin: address 60 | emergency_admin: address 61 | 62 | event ApplyAdmins: 63 | ownership_admin: address 64 | parameter_admin: address 65 | emergency_admin: address 66 | 67 | 68 | FACTORY: public(immutable(address)) 69 | OLD_MANAGER_PROXY: public(immutable(address)) 70 | 71 | 72 | ownership_admin: public(address) 73 | parameter_admin: public(address) 74 | emergency_admin: public(address) 75 | 76 | future_ownership_admin: public(address) 77 | future_parameter_admin: public(address) 78 | future_emergency_admin: public(address) 79 | 80 | gauge_manager: public(HashMap[address, address]) 81 | 82 | 83 | @external 84 | def __init__( 85 | _ownership_admin: address, 86 | _parameter_admin: address, 87 | _emergency_admin: address, 88 | _factory: address, 89 | _old_manager_proxy: address, 90 | ): 91 | FACTORY = _factory 92 | OLD_MANAGER_PROXY = _old_manager_proxy 93 | 94 | self.ownership_admin = _ownership_admin 95 | self.parameter_admin = _parameter_admin 96 | self.emergency_admin = _emergency_admin 97 | 98 | 99 | @external 100 | def deploy_plain_pool( 101 | _name: String[32], 102 | _symbol: String[10], 103 | _coins: address[4], 104 | _A: uint256, 105 | _fee: uint256, 106 | _asset_type: uint256 = 0, 107 | _implementation_idx: uint256 = 0, 108 | _ma_exp_time: uint256 = 600 109 | ) -> address: 110 | pool: address = Factory(FACTORY).deploy_plain_pool( 111 | _name, 112 | _symbol, 113 | _coins, 114 | _A, 115 | _fee, 116 | _asset_type, 117 | _implementation_idx, 118 | ) 119 | if _ma_exp_time != 600: 120 | Curve(pool).set_ma_exp_time(_ma_exp_time) 121 | return pool 122 | 123 | 124 | @external 125 | def deploy_gauge(_pool: address, _manager: address = msg.sender) -> address: 126 | """ 127 | @notice Deploy a gauge, and set _manager as the manager 128 | @param _pool The pool to deploy a gauge for 129 | @param _manager The account to which will manage rewards for the gauge 130 | """ 131 | gauge: address = Factory(FACTORY).deploy_gauge(_pool) 132 | 133 | self.gauge_manager[gauge] = _manager 134 | return gauge 135 | 136 | 137 | @external 138 | def commit_set_admins(_o_admin: address, _p_admin: address, _e_admin: address): 139 | """ 140 | @notice Set ownership admin to `_o_admin`, parameter admin to `_p_admin` and emergency admin to `_e_admin` 141 | @param _o_admin Ownership admin 142 | @param _p_admin Parameter admin 143 | @param _e_admin Emergency admin 144 | """ 145 | assert msg.sender == self.ownership_admin, "Access denied" 146 | 147 | self.future_ownership_admin = _o_admin 148 | self.future_parameter_admin = _p_admin 149 | self.future_emergency_admin = _e_admin 150 | 151 | log CommitAdmins(_o_admin, _p_admin, _e_admin) 152 | 153 | 154 | @external 155 | def apply_set_admins(): 156 | """ 157 | @notice Apply the effects of `commit_set_admins` 158 | """ 159 | assert msg.sender == self.ownership_admin, "Access denied" 160 | 161 | _o_admin: address = self.future_ownership_admin 162 | _p_admin: address = self.future_parameter_admin 163 | _e_admin: address = self.future_emergency_admin 164 | self.ownership_admin = _o_admin 165 | self.parameter_admin = _p_admin 166 | self.emergency_admin = _e_admin 167 | 168 | log ApplyAdmins(_o_admin, _p_admin, _e_admin) 169 | 170 | 171 | @external 172 | def set_ma_exp_time(_pool: address, _ma_exp_time: uint256): 173 | assert msg.sender == self.parameter_admin, "Access denied" 174 | Curve(_pool).set_ma_exp_time(_ma_exp_time) 175 | 176 | 177 | @external 178 | def commit_new_fee(_pool: address, _new_fee: uint256): 179 | assert msg.sender == self.parameter_admin, "Access denied" 180 | 181 | Curve(_pool).commit_new_fee(_new_fee) 182 | 183 | 184 | @external 185 | def apply_new_fee(_pool: address): 186 | Curve(_pool).apply_new_fee() 187 | 188 | 189 | @external 190 | @nonreentrant('lock') 191 | def ramp_A(_pool: address, _future_A: uint256, _future_time: uint256): 192 | """ 193 | @notice Start gradually increasing A of `_pool` reaching `_future_A` at `_future_time` time 194 | @param _pool Pool address 195 | @param _future_A Future A 196 | @param _future_time Future time 197 | """ 198 | assert msg.sender == self.parameter_admin, "Access denied" 199 | Curve(_pool).ramp_A(_future_A, _future_time) 200 | 201 | 202 | @external 203 | @nonreentrant('lock') 204 | def stop_ramp_A(_pool: address): 205 | """ 206 | @notice Stop gradually increasing A of `_pool` 207 | @param _pool Pool address 208 | """ 209 | assert msg.sender in [self.parameter_admin, self.emergency_admin], "Access denied" 210 | Curve(_pool).stop_ramp_A() 211 | 212 | 213 | @external 214 | def add_base_pool( 215 | _target: address, 216 | _base_pool: address, 217 | _fee_receiver: address, 218 | _asset_type: uint256, 219 | _implementations: address[10], 220 | ): 221 | assert msg.sender == self.ownership_admin, "Access denied" 222 | 223 | Factory(_target).add_base_pool(_base_pool, _fee_receiver, _asset_type, _implementations) 224 | 225 | 226 | @external 227 | def set_metapool_implementations( 228 | _target: address, 229 | _base_pool: address, 230 | _implementations: address[10], 231 | ): 232 | """ 233 | @notice Set implementation contracts for a metapool 234 | @dev Only callable by admin 235 | @param _base_pool Pool address to add 236 | @param _implementations Implementation address to use when deploying metapools 237 | """ 238 | assert msg.sender == self.ownership_admin, "Access denied" 239 | Factory(_target).set_metapool_implementations(_base_pool, _implementations) 240 | 241 | 242 | @external 243 | def set_plain_implementations( 244 | _target: address, 245 | _n_coins: uint256, 246 | _implementations: address[10], 247 | ): 248 | assert msg.sender == self.ownership_admin, "Access denied" 249 | Factory(_target).set_plain_implementations(_n_coins, _implementations) 250 | 251 | 252 | @external 253 | def set_gauge_implementation(_target: address, _gauge_implementation: address): 254 | assert msg.sender == self.ownership_admin, "Access denied" 255 | Factory(_target).set_gauge_implementation(_gauge_implementation) 256 | 257 | 258 | @external 259 | def set_fee_receiver(_target: address, _base_pool: address, _fee_receiver: address): 260 | assert msg.sender == self.ownership_admin, "Access denied" 261 | Factory(_target).set_fee_receiver(_base_pool, _fee_receiver) 262 | 263 | 264 | @external 265 | def set_factory_manager(_target: address, _manager: address): 266 | assert msg.sender in [self.ownership_admin, self.emergency_admin], "Access denied" 267 | Factory(_target).set_manager(_manager) 268 | 269 | 270 | @external 271 | def set_gauge_manager(_gauge: address, _manager: address): 272 | """ 273 | @notice Set the manager 274 | @dev Callable by the admin or existing manager 275 | @param _manager Manager address 276 | """ 277 | assert msg.sender in [self.ownership_admin, self.emergency_admin, self.gauge_manager[_gauge]], "Access denied" 278 | 279 | self.gauge_manager[_gauge] = _manager 280 | 281 | 282 | @external 283 | def commit_transfer_ownership(_target: address, _new_admin: address): 284 | """ 285 | @notice Transfer ownership of `_target` to `_new_admin` 286 | @param _target `Factory` deployment address 287 | @param _new_admin New admin address 288 | """ 289 | assert msg.sender == self.ownership_admin # dev: admin only 290 | 291 | Factory(_target).commit_transfer_ownership(_new_admin) 292 | 293 | 294 | @external 295 | def accept_transfer_ownership(_target: address): 296 | """ 297 | @notice Accept a pending ownership transfer 298 | @param _target `Factory` deployment address 299 | """ 300 | Factory(_target).accept_transfer_ownership() 301 | 302 | 303 | @external 304 | def set_killed(_gauge: address, _is_killed: bool): 305 | assert msg.sender in [self.ownership_admin, self.emergency_admin] 306 | Gauge(_gauge).set_killed(_is_killed) 307 | 308 | 309 | @external 310 | def add_reward(_gauge: address, _reward_token: address, _distributor: address): 311 | assert msg.sender in [self.ownership_admin, self.gauge_manager[_gauge]] 312 | Gauge(_gauge).add_reward(_reward_token, _distributor) 313 | 314 | 315 | @external 316 | def set_reward_distributor(_gauge: address, _reward_token: address, _distributor: address): 317 | assert msg.sender in [self.ownership_admin, self.gauge_manager[_gauge]] 318 | Gauge(_gauge).set_reward_distributor(_reward_token, _distributor) 319 | 320 | 321 | @external 322 | def migrate_gauge_manager(_gauge: address): 323 | manager: address = ManagerProxy(OLD_MANAGER_PROXY).gauge_manager(_gauge) 324 | if manager != empty(address) and self.gauge_manager[_gauge] == empty(address): 325 | self.gauge_manager[_gauge] = manager 326 | -------------------------------------------------------------------------------- /contracts/PoolMigrator.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.15 2 | """ 3 | @title Pool Migrator 4 | @author Curve.fi 5 | @notice Zap for moving liquidity between Curve factory pools in a single transaction 6 | @license MIT 7 | """ 8 | 9 | interface ERC20: 10 | def approve(_spender: address, _amount: uint256): nonpayable 11 | 12 | interface Swap: 13 | def transferFrom(_owner: address, _spender: address, _amount: uint256) -> bool: nonpayable 14 | def add_liquidity(_amounts: uint256[2], _min_mint_amount: uint256, _receiver: address) -> uint256: nonpayable 15 | def remove_liquidity(_burn_amount: uint256, _min_amounts: uint256[2]) -> uint256[2]: nonpayable 16 | def coins(i: uint256) -> address: view 17 | 18 | 19 | # pool -> coins are approved? 20 | is_approved: HashMap[address, bool] 21 | 22 | 23 | @external 24 | def migrate_to_new_pool(_old_pool: address, _new_pool: address, _amount: uint256) -> uint256: 25 | """ 26 | @notice Migrate liquidity between two pools 27 | @dev Each pool must be deployed by the curve factory and contain identical 28 | assets. The migrator must have approval to transfer `_old_pool` tokens 29 | on behalf of the caller. 30 | @param _old_pool Address of the pool to migrate from 31 | @param _new_pool Address of the pool to migrate into 32 | @param _amount Number of `_old_pool` LP tokens to migrate 33 | @return uint256 Number of `_new_pool` LP tokens received 34 | """ 35 | Swap(_old_pool).transferFrom(msg.sender, self, _amount) 36 | amounts: uint256[2] = Swap(_old_pool).remove_liquidity(_amount, [0, 0]) 37 | 38 | if not self.is_approved[_new_pool]: 39 | for i in range(2): 40 | coin: address = Swap(_new_pool).coins(i) 41 | ERC20(coin).approve(_new_pool, MAX_UINT256) 42 | self.is_approved[_new_pool] = True 43 | 44 | return Swap(_new_pool).add_liquidity(amounts, 0, msg.sender) 45 | -------------------------------------------------------------------------------- /contracts/testing/AaveLendingPoolMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | 4 | interface IERC20 { 5 | function transferFrom(address, address, uint256) external; 6 | } 7 | 8 | interface aToken { 9 | function mint(address _to, uint256 _amount) external; 10 | function redeem(address _from, address _to, uint256 _amount) external; 11 | } 12 | 13 | contract AaveLendingPoolMock { 14 | 15 | uint16 public lastReferral; 16 | 17 | mapping (address => address) aTokens; 18 | 19 | function _add_token(address _underlying, address _aToken) external { 20 | aTokens[_underlying] = _aToken; 21 | } 22 | 23 | /** 24 | * @dev deposits The underlying asset into the reserve. A corresponding amount 25 | of the overlying asset (aTokens) is minted. 26 | * @param _reserve the address of the reserve 27 | * @param _amount the amount to be deposited 28 | * @param _referralCode integrators are assigned a referral code and can potentially receive rewards. 29 | **/ 30 | function deposit(address _reserve, uint256 _amount, address _receiver, uint16 _referralCode) external payable { 31 | require (aTokens[_reserve] != address(0)); 32 | lastReferral = _referralCode; 33 | IERC20(_reserve).transferFrom(_receiver, aTokens[_reserve], _amount); 34 | aToken(aTokens[_reserve]).mint(_receiver, _amount); 35 | } 36 | 37 | function withdraw(address asset, uint256 amount, address to) external { 38 | require (aTokens[asset] != address(0)); 39 | aToken(aTokens[asset]).redeem(msg.sender, to, amount); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/testing/AddressProvider.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.15 2 | """ 3 | @title Curve Registry Address Provider Mock 4 | @license MIT 5 | @author Curve.Fi 6 | """ 7 | 8 | event NewAddressIdentifier: 9 | id: indexed(uint256) 10 | addr: address 11 | description: String[64] 12 | 13 | event AddressModified: 14 | id: indexed(uint256) 15 | new_address: address 16 | version: uint256 17 | 18 | event CommitNewAdmin: 19 | deadline: indexed(uint256) 20 | admin: indexed(address) 21 | 22 | event NewAdmin: 23 | admin: indexed(address) 24 | 25 | 26 | struct AddressInfo: 27 | addr: address 28 | is_active: bool 29 | version: uint256 30 | last_modified: uint256 31 | description: String[64] 32 | 33 | 34 | registry: address 35 | admin: public(address) 36 | transfer_ownership_deadline: public(uint256) 37 | future_admin: public(address) 38 | 39 | queue_length: uint256 40 | get_id_info: public(HashMap[uint256, AddressInfo]) 41 | 42 | 43 | @external 44 | def __init__(_admin: address): 45 | self.admin = _admin 46 | self.queue_length = 1 47 | self.get_id_info[0].description = "Main Registry" 48 | 49 | 50 | @view 51 | @external 52 | def get_registry() -> address: 53 | """ 54 | @notice Get the address of the main registry contract 55 | @dev This is a gas-efficient way of calling `AddressProvider.get_address(0)` 56 | @return address main registry contract 57 | """ 58 | return self.registry 59 | 60 | 61 | @view 62 | @external 63 | def max_id() -> uint256: 64 | """ 65 | @notice Get the highest ID set within the address provider 66 | @return uint256 max ID 67 | """ 68 | return self.queue_length - 1 69 | 70 | 71 | @view 72 | @external 73 | def get_address(_id: uint256) -> address: 74 | """ 75 | @notice Fetch the address associated with `_id` 76 | @dev Returns ZERO_ADDRESS if `_id` has not been defined, or has been unset 77 | @param _id Identifier to fetch an address for 78 | @return Current address associated to `_id` 79 | """ 80 | return self.get_id_info[_id].addr 81 | 82 | 83 | @external 84 | def add_new_id(_address: address, _description: String[64]) -> uint256: 85 | """ 86 | @notice Add a new identifier to the registry 87 | @dev ID is auto-incremented 88 | @param _address Initial address to assign to new identifier 89 | @param _description Human-readable description of the identifier 90 | @return uint256 identifier 91 | """ 92 | assert msg.sender == self.admin # dev: admin-only function 93 | assert _address.is_contract # dev: not a contract 94 | 95 | id: uint256 = self.queue_length 96 | self.get_id_info[id] = AddressInfo({ 97 | addr: _address, 98 | is_active: True, 99 | version: 1, 100 | last_modified: block.timestamp, 101 | description: _description 102 | }) 103 | self.queue_length = id + 1 104 | 105 | log NewAddressIdentifier(id, _address, _description) 106 | 107 | return id 108 | 109 | 110 | @external 111 | def set_address(_id: uint256, _address: address) -> bool: 112 | """ 113 | @notice Set a new address for an existing identifier 114 | @param _id Identifier to set the new address for 115 | @param _address Address to set 116 | @return bool success 117 | """ 118 | assert msg.sender == self.admin # dev: admin-only function 119 | assert _address.is_contract # dev: not a contract 120 | assert self.queue_length > _id # dev: id does not exist 121 | 122 | version: uint256 = self.get_id_info[_id].version + 1 123 | 124 | self.get_id_info[_id].addr = _address 125 | self.get_id_info[_id].is_active = True 126 | self.get_id_info[_id].version = version 127 | self.get_id_info[_id].last_modified = block.timestamp 128 | 129 | if _id == 0: 130 | self.registry = _address 131 | 132 | log AddressModified(_id, _address, version) 133 | 134 | return True 135 | 136 | 137 | @external 138 | def unset_address(_id: uint256) -> bool: 139 | """ 140 | @notice Unset an existing identifier 141 | @dev An identifier cannot ever be removed, it can only have the 142 | address unset so that it returns ZERO_ADDRESS 143 | @param _id Identifier to unset 144 | @return bool success 145 | """ 146 | assert msg.sender == self.admin # dev: admin-only function 147 | assert self.get_id_info[_id].is_active # dev: not active 148 | 149 | self.get_id_info[_id].is_active = False 150 | self.get_id_info[_id].addr = ZERO_ADDRESS 151 | self.get_id_info[_id].last_modified = block.timestamp 152 | 153 | if _id == 0: 154 | self.registry = ZERO_ADDRESS 155 | 156 | log AddressModified(_id, ZERO_ADDRESS, self.get_id_info[_id].version) 157 | 158 | return True 159 | 160 | 161 | @external 162 | def commit_transfer_ownership(_new_admin: address) -> bool: 163 | """ 164 | @notice Initiate a transfer of contract ownership 165 | @dev Once initiated, the actual transfer may be performed three days later 166 | @param _new_admin Address of the new owner account 167 | @return bool success 168 | """ 169 | assert msg.sender == self.admin # dev: admin-only function 170 | assert self.transfer_ownership_deadline == 0 # dev: transfer already active 171 | 172 | deadline: uint256 = block.timestamp + 3*86400 173 | self.transfer_ownership_deadline = deadline 174 | self.future_admin = _new_admin 175 | 176 | log CommitNewAdmin(deadline, _new_admin) 177 | 178 | return True 179 | 180 | 181 | @external 182 | def apply_transfer_ownership() -> bool: 183 | """ 184 | @notice Finalize a transfer of contract ownership 185 | @dev May only be called by the current owner, three days after a 186 | call to `commit_transfer_ownership` 187 | @return bool success 188 | """ 189 | assert msg.sender == self.admin # dev: admin-only function 190 | assert self.transfer_ownership_deadline != 0 # dev: transfer not active 191 | assert block.timestamp >= self.transfer_ownership_deadline # dev: now < deadline 192 | 193 | new_admin: address = self.future_admin 194 | self.admin = new_admin 195 | self.transfer_ownership_deadline = 0 196 | 197 | log NewAdmin(new_admin) 198 | 199 | return True 200 | 201 | 202 | @external 203 | def revert_transfer_ownership() -> bool: 204 | """ 205 | @notice Revert a transfer of contract ownership 206 | @dev May only be called by the current owner 207 | @return bool success 208 | """ 209 | assert msg.sender == self.admin # dev: admin-only function 210 | 211 | self.transfer_ownership_deadline = 0 212 | 213 | return True 214 | -------------------------------------------------------------------------------- /contracts/testing/CurveTokenV3.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @title Curve LP Token 4 | @author Curve.Fi 5 | @notice Base implementation for an LP token provided for 6 | supplying liquidity to `StableSwap` 7 | @dev Follows the ERC-20 token standard as defined at 8 | https://eips.ethereum.org/EIPS/eip-20 9 | """ 10 | 11 | from vyper.interfaces import ERC20 12 | 13 | implements: ERC20 14 | 15 | interface Curve: 16 | def owner() -> address: view 17 | 18 | 19 | event Transfer: 20 | _from: indexed(address) 21 | _to: indexed(address) 22 | _value: uint256 23 | 24 | event Approval: 25 | _owner: indexed(address) 26 | _spender: indexed(address) 27 | _value: uint256 28 | 29 | 30 | name: public(String[64]) 31 | symbol: public(String[32]) 32 | 33 | balanceOf: public(HashMap[address, uint256]) 34 | allowance: public(HashMap[address, HashMap[address, uint256]]) 35 | totalSupply: public(uint256) 36 | 37 | minter: public(address) 38 | 39 | 40 | @external 41 | def __init__(_name: String[64], _symbol: String[32]): 42 | self.name = _name 43 | self.symbol = _symbol 44 | self.minter = msg.sender 45 | log Transfer(ZERO_ADDRESS, msg.sender, 0) 46 | 47 | 48 | @view 49 | @external 50 | def decimals() -> uint256: 51 | """ 52 | @notice Get the number of decimals for this token 53 | @dev Implemented as a view method to reduce gas costs 54 | @return uint256 decimal places 55 | """ 56 | return 18 57 | 58 | 59 | @external 60 | def transfer(_to : address, _value : uint256) -> bool: 61 | """ 62 | @dev Transfer token for a specified address 63 | @param _to The address to transfer to. 64 | @param _value The amount to be transferred. 65 | """ 66 | # NOTE: vyper does not allow underflows 67 | # so the following subtraction would revert on insufficient balance 68 | self.balanceOf[msg.sender] -= _value 69 | self.balanceOf[_to] += _value 70 | 71 | log Transfer(msg.sender, _to, _value) 72 | return True 73 | 74 | 75 | @external 76 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 77 | """ 78 | @dev Transfer tokens from one address to another. 79 | @param _from address The address which you want to send tokens from 80 | @param _to address The address which you want to transfer to 81 | @param _value uint256 the amount of tokens to be transferred 82 | """ 83 | self.balanceOf[_from] -= _value 84 | self.balanceOf[_to] += _value 85 | 86 | _allowance: uint256 = self.allowance[_from][msg.sender] 87 | if _allowance != MAX_UINT256: 88 | self.allowance[_from][msg.sender] = _allowance - _value 89 | 90 | log Transfer(_from, _to, _value) 91 | return True 92 | 93 | 94 | @external 95 | def approve(_spender : address, _value : uint256) -> bool: 96 | """ 97 | @notice Approve the passed address to transfer the specified amount of 98 | tokens on behalf of msg.sender 99 | @dev Beware that changing an allowance via this method brings the risk 100 | that someone may use both the old and new allowance by unfortunate 101 | transaction ordering. This may be mitigated with the use of 102 | {increaseAllowance} and {decreaseAllowance}. 103 | https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 104 | @param _spender The address which will transfer the funds 105 | @param _value The amount of tokens that may be transferred 106 | @return bool success 107 | """ 108 | self.allowance[msg.sender][_spender] = _value 109 | 110 | log Approval(msg.sender, _spender, _value) 111 | return True 112 | 113 | 114 | @external 115 | def increaseAllowance(_spender: address, _added_value: uint256) -> bool: 116 | """ 117 | @notice Increase the allowance granted to `_spender` by the caller 118 | @dev This is alternative to {approve} that can be used as a mitigation for 119 | the potential race condition 120 | @param _spender The address which will transfer the funds 121 | @param _added_value The amount of to increase the allowance 122 | @return bool success 123 | """ 124 | allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value 125 | self.allowance[msg.sender][_spender] = allowance 126 | 127 | log Approval(msg.sender, _spender, allowance) 128 | return True 129 | 130 | 131 | @external 132 | def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: 133 | """ 134 | @notice Decrease the allowance granted to `_spender` by the caller 135 | @dev This is alternative to {approve} that can be used as a mitigation for 136 | the potential race condition 137 | @param _spender The address which will transfer the funds 138 | @param _subtracted_value The amount of to decrease the allowance 139 | @return bool success 140 | """ 141 | allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value 142 | self.allowance[msg.sender][_spender] = allowance 143 | 144 | log Approval(msg.sender, _spender, allowance) 145 | return True 146 | 147 | 148 | @external 149 | def mint(_to: address, _value: uint256) -> bool: 150 | """ 151 | @dev Mint an amount of the token and assigns it to an account. 152 | This encapsulates the modification of balances such that the 153 | proper events are emitted. 154 | @param _to The account that will receive the created tokens. 155 | @param _value The amount that will be created. 156 | """ 157 | assert msg.sender == self.minter 158 | 159 | self.totalSupply += _value 160 | self.balanceOf[_to] += _value 161 | 162 | log Transfer(ZERO_ADDRESS, _to, _value) 163 | return True 164 | 165 | 166 | @external 167 | def burnFrom(_to: address, _value: uint256) -> bool: 168 | """ 169 | @dev Burn an amount of the token from a given account. 170 | @param _to The account whose tokens will be burned. 171 | @param _value The amount that will be burned. 172 | """ 173 | assert msg.sender == self.minter 174 | 175 | self.totalSupply -= _value 176 | self.balanceOf[_to] -= _value 177 | 178 | log Transfer(_to, ZERO_ADDRESS, _value) 179 | return True 180 | 181 | 182 | @external 183 | def set_minter(_minter: address): 184 | assert msg.sender == self.minter 185 | self.minter = _minter 186 | 187 | 188 | @external 189 | def set_name(_name: String[64], _symbol: String[32]): 190 | assert Curve(self.minter).owner() == msg.sender 191 | self.name = _name 192 | self.symbol = _symbol -------------------------------------------------------------------------------- /contracts/testing/ERC20Mock.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock ERC20 for testing 4 | """ 5 | 6 | event Transfer: 7 | _from: indexed(address) 8 | _to: indexed(address) 9 | _value: uint256 10 | 11 | event Approval: 12 | _owner: indexed(address) 13 | _spender: indexed(address) 14 | _value: uint256 15 | 16 | name: public(String[64]) 17 | symbol: public(String[32]) 18 | decimals: public(uint256) 19 | balanceOf: public(HashMap[address, uint256]) 20 | allowances: HashMap[address, HashMap[address, uint256]] 21 | total_supply: uint256 22 | 23 | 24 | @external 25 | def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): 26 | self.name = _name 27 | self.symbol = _symbol 28 | self.decimals = _decimals 29 | 30 | 31 | @external 32 | @view 33 | def totalSupply() -> uint256: 34 | return self.total_supply 35 | 36 | 37 | @external 38 | @view 39 | def allowance(_owner : address, _spender : address) -> uint256: 40 | return self.allowances[_owner][_spender] 41 | 42 | 43 | @external 44 | def transfer(_to : address, _value : uint256) -> bool: 45 | self.balanceOf[msg.sender] -= _value 46 | self.balanceOf[_to] += _value 47 | log Transfer(msg.sender, _to, _value) 48 | return True 49 | 50 | 51 | @external 52 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 53 | self.balanceOf[_from] -= _value 54 | self.balanceOf[_to] += _value 55 | self.allowances[_from][msg.sender] -= _value 56 | log Transfer(_from, _to, _value) 57 | return True 58 | 59 | 60 | @external 61 | def approve(_spender : address, _value : uint256) -> bool: 62 | self.allowances[msg.sender][_spender] = _value 63 | log Approval(msg.sender, _spender, _value) 64 | return True 65 | 66 | 67 | @external 68 | def _mint_for_testing(_target: address, _value: uint256) -> bool: 69 | self.total_supply += _value 70 | self.balanceOf[_target] += _value 71 | log Transfer(ZERO_ADDRESS, _target, _value) 72 | 73 | return True 74 | -------------------------------------------------------------------------------- /contracts/testing/MockBoostDelegationProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.15 2 | """ 3 | @title Mock Boost Delegation 4 | """ 5 | from vyper.interfaces import ERC20 6 | 7 | delegation: public(address) 8 | voting_escrow: public(address) 9 | 10 | 11 | @external 12 | def __init__(voting_escrow: address, _delegation: address): 13 | self.delegation = _delegation 14 | self.voting_escrow = voting_escrow 15 | 16 | 17 | @view 18 | @external 19 | def adjusted_balance_of(_addr: address) -> uint256: 20 | if self.delegation == ZERO_ADDRESS: 21 | return ERC20(self.voting_escrow).balanceOf(_addr) 22 | # simulated behavior being that we'd call the actual 23 | # veboost contract 24 | # which would look like 25 | # Veboost(addr).adjusted_balance_of(_addr) 26 | # but for the most part we are just afirming that this 27 | # contract is called not the implementation of veboost 28 | return 69 29 | 30 | 31 | @external 32 | def set_delegation(_delegation: address): 33 | self.delegation = _delegation 34 | -------------------------------------------------------------------------------- /contracts/testing/aERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import {AaveLendingPoolMock} from "./AaveLendingPoolMock.sol"; 4 | 5 | library SafeMath { 6 | function add(uint256 a, uint256 b) internal pure returns (uint256 c) { 7 | c = a + b; 8 | require(c >= a); // dev: overflow 9 | } 10 | 11 | function sub(uint256 a, uint256 b) internal pure returns (uint256 c) { 12 | require(b <= a); // dev: underflow 13 | c = a - b; 14 | } 15 | 16 | function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { 17 | c = a * b; 18 | require(a == 0 || c / a == b); // dev: overflow 19 | } 20 | 21 | function div(uint256 a, uint256 b) internal pure returns (uint256 c) { 22 | require(b > 0); // dev: divide by zero 23 | c = a / b; 24 | } 25 | } 26 | 27 | interface IERC20 { 28 | function transfer(address, uint256) external; 29 | 30 | function transferFrom( 31 | address, 32 | address, 33 | uint256 34 | ) external; 35 | 36 | function decimals() external returns (uint256); 37 | 38 | function _mint_for_testing(address, uint256) external; 39 | } 40 | 41 | contract ATokenMock { 42 | using SafeMath for uint256; 43 | 44 | string public symbol; 45 | string public name; 46 | uint256 public decimals; 47 | uint256 public totalSupply; 48 | 49 | address pool; 50 | 51 | // included for compatibility with test suite - always returns 10**18 52 | uint256 public _get_rate; 53 | 54 | address lendingPool; 55 | IERC20 underlyingToken; 56 | 57 | mapping(address => uint256) balances; 58 | mapping(address => mapping(address => uint256)) allowed; 59 | 60 | event Transfer(address from, address to, uint256 value); 61 | event Approval(address owner, address spender, uint256 value); 62 | 63 | constructor( 64 | string memory _name, 65 | string memory _symbol, 66 | uint256 _decimals, 67 | address _underlyingToken, 68 | address _lendingPool 69 | ) public { 70 | symbol = _symbol; 71 | name = _name; 72 | decimals = _decimals; 73 | underlyingToken = IERC20(_underlyingToken); 74 | lendingPool = _lendingPool; 75 | _get_rate = 10**18; 76 | AaveLendingPoolMock(_lendingPool)._add_token( 77 | _underlyingToken, 78 | address(this) 79 | ); 80 | } 81 | 82 | function balanceOf(address _owner) public view returns (uint256) { 83 | return balances[_owner]; 84 | } 85 | 86 | function allowance(address _owner, address _spender) 87 | public 88 | view 89 | returns (uint256) 90 | { 91 | return allowed[_owner][_spender]; 92 | } 93 | 94 | function approve(address _spender, uint256 _value) public returns (bool) { 95 | allowed[msg.sender][_spender] = _value; 96 | emit Approval(msg.sender, _spender, _value); 97 | return true; 98 | } 99 | 100 | function transfer(address _to, uint256 _value) public returns (bool) { 101 | balances[msg.sender] = balances[msg.sender].sub(_value); 102 | balances[_to] = balances[_to].add(_value); 103 | emit Transfer(msg.sender, _to, _value); 104 | return true; 105 | } 106 | 107 | function transferFrom( 108 | address _from, 109 | address _to, 110 | uint256 _value 111 | ) public returns (bool) { 112 | balances[_from] = balances[_from].sub(_value); 113 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); 114 | balances[_to] = balances[_to].add(_value); 115 | emit Transfer(_from, _to, _value); 116 | return true; 117 | } 118 | 119 | function redeem( 120 | address _from, 121 | address _to, 122 | uint256 _amount 123 | ) external { 124 | totalSupply = totalSupply.sub(_amount); 125 | balances[_from] = balances[_from].sub(_amount); 126 | underlyingToken.transfer(_to, _amount); 127 | emit Transfer(_from, address(0), _amount); 128 | } 129 | 130 | function mint(address _to, uint256 _amount) external { 131 | require(msg.sender == lendingPool); 132 | totalSupply = totalSupply.add(_amount); 133 | balances[_to] = balances[_to].add(_amount); 134 | emit Transfer(address(0), _to, _amount); 135 | } 136 | 137 | function _set_pool(address _pool) external { 138 | pool = _pool; 139 | } 140 | 141 | function set_exchange_rate(uint256 _rate) external { 142 | /** 143 | Because aTokens accrue balance instead of increasing their rate, 144 | we simulate a rate change by modifying the token balance of the pool 145 | */ 146 | totalSupply = totalSupply.sub(balances[pool]); 147 | balances[pool] = balances[pool].mul(_rate).div(10**18); 148 | totalSupply = totalSupply.add(balances[pool]); 149 | } 150 | 151 | function _mint_for_testing(address _to, uint256 _amount) external { 152 | totalSupply = totalSupply.add(_amount); 153 | balances[_to] = balances[_to].add(_amount); 154 | underlyingToken._mint_for_testing(address(this), _amount); 155 | emit Transfer(address(0), _to, _amount); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /contracts/testing/rERC20.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.8 2 | """ 3 | @notice Mock rETH token 4 | @dev This is for testing only, it is NOT safe for use 5 | """ 6 | 7 | from vyper.interfaces import ERC20 8 | 9 | interface ERC20Mock: 10 | def decimals() -> uint256: view 11 | def _mint_for_testing(_target: address, _value: uint256) -> bool: nonpayable 12 | 13 | 14 | event Transfer: 15 | _from: indexed(address) 16 | _to: indexed(address) 17 | _value: uint256 18 | 19 | event Approval: 20 | _owner: indexed(address) 21 | _spender: indexed(address) 22 | _value: uint256 23 | 24 | 25 | name: public(String[64]) 26 | symbol: public(String[32]) 27 | decimals: public(uint256) 28 | balanceOf: public(HashMap[address, uint256]) 29 | allowances: HashMap[address, HashMap[address, uint256]] 30 | total_supply: uint256 31 | 32 | exchangeRateStored: public(uint256) 33 | 34 | @external 35 | def __init__( 36 | _name: String[64], 37 | _symbol: String[32], 38 | _decimals: uint256, 39 | _underlying_token: address 40 | ): 41 | self.name = _name 42 | self.symbol = _symbol 43 | self.decimals = _decimals 44 | self.exchangeRateStored = 10**18 45 | 46 | 47 | @external 48 | @view 49 | def totalSupply() -> uint256: 50 | return self.total_supply 51 | 52 | 53 | @external 54 | @view 55 | def allowance(_owner : address, _spender : address) -> uint256: 56 | return self.allowances[_owner][_spender] 57 | 58 | 59 | @external 60 | def transfer(_to : address, _value : uint256) -> bool: 61 | self.balanceOf[msg.sender] -= _value 62 | self.balanceOf[_to] += _value 63 | log Transfer(msg.sender, _to, _value) 64 | return True 65 | 66 | 67 | @external 68 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 69 | self.balanceOf[_from] -= _value 70 | self.balanceOf[_to] += _value 71 | self.allowances[_from][msg.sender] -= _value 72 | log Transfer(_from, _to, _value) 73 | return True 74 | 75 | 76 | @external 77 | def approve(_spender : address, _value : uint256) -> bool: 78 | self.allowances[msg.sender][_spender] = _value 79 | log Approval(msg.sender, _spender, _value) 80 | return True 81 | 82 | 83 | # rETH-specific function 84 | @view 85 | @external 86 | def getExchangeRate() -> uint256: 87 | rate: uint256 = self.exchangeRateStored 88 | return rate 89 | 90 | 91 | @external 92 | def set_exchange_rate(rate: uint256): 93 | self.exchangeRateStored = rate 94 | 95 | 96 | @external 97 | def _mint_for_testing(_target: address, _value: uint256) -> bool: 98 | self.total_supply += _value 99 | self.balanceOf[_target] += _value 100 | log Transfer(ZERO_ADDRESS, _target, _value) 101 | 102 | return True -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "preinstall": "npm i -g ganache-cli@6.12.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 100 3 | target-version = ['py36', 'py37', 'py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.eggs 8 | | \.git 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | | env 18 | | venv 19 | )/ 20 | ''' 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | brownie-token-tester==0.3.2 2 | eth-brownie>=1.15.1,<2.0.0 3 | black 4 | flake8==3.8.4 5 | isort==5.7.0 6 | -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | from brownie import ( 2 | DepositZapBTC, 3 | DepositZapUSD, 4 | Factory, 5 | MetaImplementationBTC, 6 | MetaImplementationUSD, 7 | OwnerProxy, 8 | accounts, 9 | ) 10 | from brownie.network.gas.strategies import GasNowScalingStrategy 11 | 12 | # modify me prior to deployment on mainnet! 13 | DEPLOYER = accounts.at("0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a", force=True) 14 | 15 | gas_price = GasNowScalingStrategy("slow", "fast") 16 | 17 | 18 | OWNER_ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" 19 | PARAM_ADMIN = "0x4EEb3bA4f221cA16ed4A0cC7254E2E32DF948c5f" 20 | EMERGENCY_ADMIN = "0x00669DF67E4827FCc0E48A1838a8d5AB79281909" 21 | 22 | BASE_3POOL = "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7" 23 | BASE_SBTC = "0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714" 24 | 25 | FEE_RECEIVER_USD = "0xa464e6dcda8ac41e03616f95f4bc98a13b8922dc" 26 | FEE_RECEIVER_BTC = "0xf9fc73496484290142ee856639f69e04465985cd" 27 | 28 | 29 | def main(deployer=DEPLOYER): 30 | factory = Factory.deploy({"from": deployer}) 31 | 32 | implementation_usd = MetaImplementationUSD.deploy({"from": deployer, "gas_price": gas_price}) 33 | factory.add_base_pool( 34 | BASE_3POOL, implementation_usd, FEE_RECEIVER_USD, {"from": deployer, "gas_price": gas_price} 35 | ) 36 | 37 | implementation_btc = MetaImplementationBTC.deploy({"from": deployer, "gas_price": gas_price}) 38 | factory.add_base_pool( 39 | BASE_SBTC, implementation_btc, FEE_RECEIVER_BTC, {"from": deployer, "gas_price": gas_price} 40 | ) 41 | 42 | proxy = OwnerProxy.deploy( 43 | OWNER_ADMIN, PARAM_ADMIN, EMERGENCY_ADMIN, {"from": deployer, "gas_price": gas_price} 44 | ) 45 | 46 | factory.commit_transfer_ownership(proxy, {"from": deployer, "gas_price": gas_price}) 47 | proxy.accept_transfer_ownership(factory, {"from": deployer, "gas_price": gas_price}) 48 | 49 | DepositZapUSD.deploy({"from": deployer, "gas_price": gas_price}) 50 | DepositZapBTC.deploy({"from": deployer, "gas_price": gas_price}) 51 | -------------------------------------------------------------------------------- /scripts/deploy_templates.py: -------------------------------------------------------------------------------- 1 | """ 2 | Deploy gauge extension and metapool implementation contracts. 3 | 4 | Constants should be set here for substitution in the contracts. 5 | """ 6 | from pathlib import Path 7 | 8 | from brownie import ZERO_ADDRESS, MetaBalances, MetaStandard, accounts, compile_source 9 | 10 | DEPLOYER = accounts.at("0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a", force=True) 11 | 12 | # where we will store the modified source files 13 | Path("tmp").mkdir(parents=True, exist_ok=True) 14 | 15 | 16 | # CHANGE PRIOR TO DEPLOYMENT 17 | 18 | FACTORY = ZERO_ADDRESS 19 | 20 | BASE_POOL = ZERO_ADDRESS 21 | BASE_COINS = [ 22 | ZERO_ADDRESS, 23 | ZERO_ADDRESS, 24 | ZERO_ADDRESS, 25 | ] 26 | BASE_LP_TOKEN = ZERO_ADDRESS 27 | 28 | 29 | def deploy_meta_implementation(_implementation_source: str): 30 | 31 | source = _implementation_source 32 | source = source.replace("69", str(len(BASE_COINS)), 1) 33 | source = source.replace(f"[{', '.join([ZERO_ADDRESS] * 69)}]", f"[{', '.join(BASE_COINS)}]") 34 | for addr in [BASE_POOL, BASE_LP_TOKEN]: 35 | source = source.replace(f"= {ZERO_ADDRESS}", f"= {addr}", 1) 36 | 37 | META = compile_source(source).Vyper 38 | meta = META.deploy({"from": DEPLOYER}) 39 | 40 | with open(f"tmp/{meta.address}", "w") as f: 41 | f.write(source) 42 | 43 | return meta.address 44 | 45 | 46 | def main(): 47 | with open("tmp/deployments.txt", "a") as f: 48 | f.write(f"Base Pool Templates - {BASE_POOL}\n") 49 | 50 | for implementation in [MetaStandard, MetaBalances]: 51 | source = implementation._build["source"] 52 | meta_impl = deploy_meta_implementation(source) 53 | 54 | with open("tmp/deployments.txt", "a") as f: 55 | f.write(f"{implementation._name} deployed at - {meta_impl}\n") 56 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | ignore = E203,W503 4 | 5 | [tool:isort] 6 | force_grid_wrap = 0 7 | include_trailing_comma = True 8 | line_length = 100 9 | multi_line_output = 3 10 | use_parentheses = True 11 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from brownie._config import CONFIG 5 | from brownie.project.main import get_loaded_projects 6 | 7 | pytest_plugins = [ 8 | "fixtures.accounts", 9 | "fixtures.coins", 10 | "fixtures.constants", 11 | "fixtures.deployments", 12 | "fixtures.functions", 13 | ] 14 | 15 | pool_types = { 16 | "basic": 0, 17 | "eth": 1, 18 | "optimized": 2, 19 | "rebase": 3, 20 | "meta-usd": 4, 21 | "meta-btc": 5, 22 | "meta-side": 6, 23 | } 24 | return_types = {"revert": 0, "False": 1, "None": 2} 25 | 26 | 27 | def pytest_addoption(parser): 28 | parser.addoption( 29 | "--plain-pool-size", 30 | action="store", 31 | default="2,3,4", 32 | help="comma-separated list of plain pool sizes to test against", 33 | ) 34 | parser.addoption( 35 | "--pool-type", 36 | action="store", 37 | default="basic,eth,optimized,rebase,meta-usd,meta-btc", 38 | help="comma-separated list of pool types to test against", 39 | ) 40 | parser.addoption( 41 | "--return-type", 42 | action="store", 43 | default="revert,False,None", 44 | help="comma-separated list of ERC20 token return types to test against", 45 | ) 46 | parser.addoption( 47 | "--decimals", 48 | action="store", 49 | default="18", 50 | help="comma-separated list of ERC20 token precisions to test against", 51 | ) 52 | 53 | 54 | def pytest_generate_tests(metafunc): 55 | if "plain_pool_size" in metafunc.fixturenames: 56 | cli_options = metafunc.config.getoption("plain_pool_size").split(",") 57 | pool_sizes = [int(v) for v in cli_options] 58 | metafunc.parametrize( 59 | "plain_pool_size", 60 | pool_sizes, 61 | indirect=True, 62 | ids=[f"(PoolSize={i})" for i in cli_options], 63 | ) 64 | if "pool_type" in metafunc.fixturenames: 65 | cli_options = metafunc.config.getoption("pool_type").split(",") 66 | pool_type_ids = [pool_types[v] for v in cli_options] 67 | metafunc.parametrize( 68 | "pool_type", 69 | pool_type_ids, 70 | indirect=True, 71 | ids=[f"(PoolType={i})" for i in cli_options], 72 | ) 73 | if "return_type" in metafunc.fixturenames: 74 | cli_options = metafunc.config.getoption("return_type").split(",") 75 | return_type_ids = [return_types[v] for v in cli_options] 76 | metafunc.parametrize( 77 | "return_type", 78 | return_type_ids, 79 | indirect=True, 80 | ids=[f"(ReturnType={i})" for i in cli_options], 81 | ) 82 | if "decimals" in metafunc.fixturenames: 83 | cli_options = metafunc.config.getoption("decimals").split(",") 84 | metafunc.parametrize( 85 | "decimals", 86 | [int(i) for i in cli_options], 87 | indirect=True, 88 | ids=[f"(Decimals={i})" for i in cli_options], 89 | ) 90 | if "meta_implementation_idx" in metafunc.fixturenames: 91 | metafunc.parametrize( 92 | "meta_implementation_idx", 93 | [0, 1], 94 | indirect=True, 95 | ids=[f"(Meta-Implementation={i})" for i in ["Standard", "Rebase"]], 96 | ) 97 | 98 | 99 | def pytest_collection_modifyitems(config, items): 100 | project = get_loaded_projects()[0] 101 | 102 | for item in items.copy(): 103 | path = Path(item.fspath).relative_to(project._path) 104 | path_parts = path.parts[1:-1] 105 | try: 106 | params = item.callspec.params 107 | pool_size = params.get("plain_pool_size", 2) 108 | pool_type = params.get("pool_type", 2) 109 | return_type = params.get("return_type", 0) 110 | decimals = params.get("decimals", 18) 111 | meta_implementation_idx = params.get("meta_implementation_idx", 0) 112 | except Exception: 113 | if path_parts == (): 114 | if pool_type != 2: 115 | items.remove(item) 116 | continue 117 | 118 | # optimized pool only supports return True/revert 119 | if pool_type == 2 and return_type != 0: 120 | items.remove(item) 121 | continue 122 | 123 | # optimized pool only supports precision == 18 124 | if pool_type == 2 and decimals != 18: 125 | items.remove(item) 126 | continue 127 | 128 | # meta pools we only test against 1 type no parameterization needed 129 | if pool_type in [4, 5, 6]: 130 | if decimals != 18: 131 | items.remove(item) 132 | continue 133 | 134 | if return_type != 0: 135 | items.remove(item) 136 | continue 137 | 138 | if pool_size > 2: 139 | items.remove(item) 140 | continue 141 | 142 | else: 143 | if meta_implementation_idx > 0: 144 | items.remove(item) 145 | continue 146 | 147 | if "zaps" in path_parts: 148 | # zap tests only apply to the meta implementations 149 | # and we only use the template zap DepositZap.vy 150 | # all the zaps are essentially copies of this with 151 | # constants set appropriately 152 | items.remove(item) 153 | continue 154 | 155 | if len(path_parts) > 1 and path_parts[1] == "rebase": 156 | if pool_type != 3: 157 | items.remove(item) 158 | continue 159 | 160 | # only allow meta pools in the meta directory 161 | if len(path_parts) > 1 and path_parts[1] == "meta": 162 | if pool_type not in [4, 5, 6]: 163 | items.remove(item) 164 | continue 165 | 166 | if pool_type != 6 and "test_sidechain_rewards.py" in path.parts: 167 | items.remove(item) 168 | continue 169 | 170 | if "test_factory.py" not in path.parts and len(path.parts) == 2: 171 | if pool_type != 2 or pool_size != 2: 172 | items.remove(item) 173 | continue 174 | 175 | # hacky magic to ensure the correct number of tests is shown in collection report 176 | config.pluginmanager.get_plugin("terminalreporter")._numcollected = len(items) 177 | 178 | 179 | @pytest.fixture(scope="session") 180 | def plain_pool_size(request): 181 | return request.param 182 | 183 | 184 | @pytest.fixture(scope="session") 185 | def pool_type(request): 186 | return request.param 187 | 188 | 189 | @pytest.fixture(scope="session") 190 | def is_eth_pool(pool_type): 191 | return pool_type == 1 192 | 193 | 194 | @pytest.fixture(scope="session") 195 | def is_rebase_pool(pool_type): 196 | return pool_type == 3 197 | 198 | 199 | @pytest.fixture(scope="session") 200 | def is_meta_pool(pool_type): 201 | return pool_type in [4, 5, 6] 202 | 203 | 204 | @pytest.fixture(scope="session") 205 | def return_type(request): 206 | return request.param 207 | 208 | 209 | @pytest.fixture(scope="session") 210 | def meta_implementation_idx(request): 211 | return request.param 212 | 213 | 214 | @pytest.fixture(scope="session") 215 | def decimals(plain_pool_size, request, is_eth_pool, is_meta_pool): 216 | if is_eth_pool: 217 | return [18] + [request.param] * (plain_pool_size - 1) 218 | elif is_meta_pool: 219 | return [request.param] + [18] 220 | return [request.param] * plain_pool_size 221 | 222 | 223 | @pytest.fixture(scope="session") 224 | def underlying_decimals(decimals, is_meta_pool): 225 | if is_meta_pool: 226 | return [decimals[0]] + [18] * 3 227 | return decimals 228 | 229 | 230 | @pytest.fixture(scope="session") 231 | def project(): 232 | return get_loaded_projects()[0] 233 | 234 | 235 | @pytest.fixture(scope="session") 236 | def is_forked(): 237 | return "fork" in CONFIG.active_network["id"] 238 | 239 | 240 | @pytest.fixture(scope="module", autouse=True) 241 | def mod_isolation(chain): 242 | chain.snapshot() 243 | yield 244 | chain.revert() 245 | 246 | 247 | @pytest.fixture(autouse=True) 248 | def isolation(chain, history): 249 | start = len(history) 250 | yield 251 | end = len(history) 252 | if end - start > 0: 253 | chain.undo(end - start) 254 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvefi/curve-factory/5272fd2b1199f185d9cd94822be9262c387dc2c2/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/accounts.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | transfer_amt = (1_000_000_000 - 1_000_000) * 10 ** 18 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def alice(accounts): 8 | accounts[0].transfer(accounts[-1], transfer_amt) 9 | return accounts[0] 10 | 11 | 12 | @pytest.fixture(scope="session") 13 | def bob(accounts): 14 | accounts[1].transfer(accounts[-1], transfer_amt) 15 | return accounts[1] 16 | 17 | 18 | @pytest.fixture(scope="session") 19 | def charlie(accounts): 20 | accounts[2].transfer(accounts[-1], accounts[2].balance()) 21 | return accounts[2] 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def dave(accounts): 26 | accounts[3].transfer(accounts[-1], transfer_amt) 27 | return accounts[3] 28 | 29 | 30 | @pytest.fixture(scope="session") 31 | def erin(accounts): 32 | accounts[4].transfer(accounts[-1], transfer_amt) 33 | return accounts[4] 34 | 35 | 36 | @pytest.fixture(scope="session") 37 | def frank(accounts): 38 | accounts[5].transfer(accounts[-1], transfer_amt) 39 | return accounts[5] 40 | 41 | 42 | @pytest.fixture(scope="session") 43 | def fee_receiver(frank): 44 | return frank 45 | -------------------------------------------------------------------------------- /tests/fixtures/coins.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS 3 | from brownie_tokens import ERC20 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def _plain_coins(alice, decimals): 8 | return_true_revert = [ERC20(decimals=precision, deployer=alice) for precision in decimals] 9 | return_true_false = [ 10 | ERC20(fail=False, decimals=precision, deployer=alice) for precision in decimals 11 | ] 12 | return_none_revert = [ 13 | ERC20(success=None, decimals=precision, deployer=alice) for precision in decimals 14 | ] 15 | return [return_true_revert, return_true_false, return_none_revert] 16 | 17 | 18 | @pytest.fixture(scope="session", autouse=True) 19 | def lp_token(alice, CurveTokenV3, accounts): 20 | lp_token = CurveTokenV3.deploy("Test LP Token", "Tester", {"from": alice}) 21 | setattr( 22 | lp_token, 23 | "_mint_for_testing", 24 | lambda _to, _amount, _tx: lp_token.transfer(_to, _amount, {"from": accounts[10]}), 25 | ) 26 | return lp_token 27 | 28 | 29 | @pytest.fixture(scope="session") 30 | def plain_coins(_plain_coins, return_type): 31 | return _plain_coins[return_type] 32 | 33 | 34 | @pytest.fixture(scope="session") 35 | def rebase_coins(alice, ATokenMock, decimals, lending_pool, plain_coins): 36 | return [ 37 | ATokenMock.deploy( 38 | "Test Rebase Token", "TRT", precision, token, lending_pool, {"from": alice} 39 | ) 40 | for precision, token in zip(decimals, plain_coins) 41 | ] 42 | 43 | 44 | @pytest.fixture(scope="session", autouse=True) 45 | def base_coins(alice): 46 | return [ERC20(deployer=alice) for _ in range(3)] 47 | 48 | 49 | @pytest.fixture(scope="session") 50 | def coins( 51 | plain_coins, 52 | rebase_coins, 53 | is_eth_pool, 54 | is_rebase_pool, 55 | is_meta_pool, 56 | lp_token, 57 | pool_type, 58 | base_gauge, 59 | ): 60 | if is_eth_pool: 61 | return [ETH_ADDRESS] + plain_coins[1:] 62 | elif is_rebase_pool: 63 | return rebase_coins 64 | elif is_meta_pool: 65 | return [plain_coins[0], lp_token] 66 | else: 67 | return plain_coins 68 | 69 | 70 | @pytest.fixture(scope="session") 71 | def underlying_coins(coins, plain_coins, is_rebase_pool, is_meta_pool, base_coins): 72 | if is_rebase_pool: 73 | return plain_coins 74 | elif is_meta_pool: 75 | return [plain_coins[0]] + base_coins 76 | else: 77 | return coins 78 | 79 | 80 | @pytest.fixture(scope="session") 81 | def underlying_decimals(decimals): 82 | return decimals 83 | 84 | 85 | @pytest.fixture(scope="session", autouse=True) 86 | def coin_a(): 87 | return ERC20() 88 | 89 | 90 | @pytest.fixture(scope="session", autouse=True) 91 | def coin_b(): 92 | return ERC20() 93 | 94 | 95 | @pytest.fixture(scope="session", autouse=True) 96 | def coin_reward(): 97 | return ERC20() 98 | -------------------------------------------------------------------------------- /tests/fixtures/constants.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="session") 5 | def initial_amounts(decimals): 6 | return [1_000_000 * 10 ** precision for precision in decimals] 7 | 8 | 9 | @pytest.fixture(scope="session") 10 | def initial_amounts_underlying(underlying_decimals): 11 | amts = [1_000_000 * 10 ** precision for precision in underlying_decimals] 12 | for i in range(1, len(underlying_decimals)): 13 | amts[i] //= 3 14 | return amts 15 | 16 | 17 | @pytest.fixture(scope="session") 18 | def deposit_amounts(decimals): 19 | return [1_000 * 10 ** precision for precision in decimals] 20 | -------------------------------------------------------------------------------- /tests/fixtures/functions.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import pytest 4 | from brownie import ETH_ADDRESS, ZERO_ADDRESS 5 | 6 | # session level functions 7 | 8 | 9 | def _deploy_plain_implementation(_project, _pool_size, _pool_type, _deployer): 10 | contract = getattr(_project, f"Plain{_pool_size}{_pool_type}") 11 | return contract.deploy({"from": _deployer}) 12 | 13 | 14 | @pytest.fixture(scope="session") 15 | def deploy_plain_implementation(alice, project): 16 | return partial(_deploy_plain_implementation, _project=project, _deployer=alice) 17 | 18 | 19 | @pytest.fixture(scope="module", autouse=True) 20 | def set_plain_implementations( 21 | alice, factory, plain_implementations, plain_pool_size, mod_isolation 22 | ): 23 | factory.set_plain_implementations( 24 | plain_pool_size, plain_implementations + [ZERO_ADDRESS] * 6, {"from": alice} 25 | ) 26 | 27 | 28 | @pytest.fixture(scope="module", autouse=True) 29 | def set_meta_implementations( 30 | alice, factory, base_pool, meta_implementations, pool_type, fee_receiver, mod_isolation 31 | ): 32 | if pool_type not in [4, 5, 6]: 33 | return 34 | if base_pool.address in [factory.base_pool_list(i) for i in range(factory.base_pool_count())]: 35 | return 36 | asset_type = 0 if pool_type == 4 else 2 if pool_type == 5 else 3 37 | factory.add_base_pool( 38 | base_pool, 39 | fee_receiver, 40 | asset_type, 41 | meta_implementations + [ZERO_ADDRESS] * 8, 42 | {"from": alice}, 43 | ) 44 | 45 | 46 | @pytest.fixture(scope="session") 47 | def set_gauge_implementation(alice, factory, gauge_implementation): 48 | factory.set_gauge_implementation(gauge_implementation, {"from": alice}) 49 | 50 | 51 | @pytest.fixture(scope="session") 52 | def eth_amount(is_eth_pool): 53 | return lambda amount: amount if is_eth_pool else 0 54 | 55 | 56 | # function level functions 57 | 58 | 59 | @pytest.fixture 60 | def mint_alice(alice, initial_amounts, coins): 61 | for coin, amount in zip(coins, initial_amounts): 62 | if coin == ETH_ADDRESS: 63 | continue 64 | coin._mint_for_testing(alice, amount, {"from": alice}) 65 | 66 | 67 | @pytest.fixture(scope="module") 68 | def approve_alice(alice, coins, swap): 69 | for coin in coins: 70 | if coin == ETH_ADDRESS: 71 | continue 72 | coin.approve(swap, 2 ** 256 - 1, {"from": alice}) 73 | 74 | 75 | @pytest.fixture 76 | def mint_bob(bob, initial_amounts, coins): 77 | for coin, amount in zip(coins, initial_amounts): 78 | if coin == ETH_ADDRESS: 79 | continue 80 | coin._mint_for_testing(bob, amount, {"from": bob}) 81 | 82 | 83 | @pytest.fixture(scope="module") 84 | def approve_bob(bob, coins, swap): 85 | for coin in coins: 86 | if coin == ETH_ADDRESS: 87 | continue 88 | coin.approve(swap, 2 ** 256 - 1, {"from": bob}) 89 | 90 | 91 | @pytest.fixture 92 | def mint_alice_underlying(alice, initial_amounts_underlying, underlying_coins): 93 | for coin, amount in zip(underlying_coins, initial_amounts_underlying): 94 | if coin == ETH_ADDRESS: 95 | continue 96 | coin._mint_for_testing(alice, amount, {"from": alice}) 97 | 98 | 99 | @pytest.fixture(scope="module") 100 | def approve_alice_underlying(alice, underlying_coins, swap): 101 | for coin in underlying_coins: 102 | if coin == ETH_ADDRESS: 103 | continue 104 | coin.approve(swap, 2 ** 256 - 1, {"from": alice}) 105 | 106 | 107 | @pytest.fixture 108 | def mint_bob_underlying(bob, initial_amounts_underlying, underlying_coins): 109 | for coin, amount in zip(underlying_coins, initial_amounts_underlying): 110 | if coin == ETH_ADDRESS: 111 | continue 112 | coin._mint_for_testing(bob, amount, {"from": bob}) 113 | 114 | 115 | @pytest.fixture(scope="module") 116 | def approve_bob_underlying(bob, underlying_coins, swap): 117 | for coin in underlying_coins: 118 | if coin == ETH_ADDRESS: 119 | continue 120 | coin.approve(swap, 2 ** 256 - 1, {"from": bob}) 121 | 122 | 123 | @pytest.fixture 124 | def add_initial_liquidity(alice, approve_alice, mint_alice, initial_amounts, swap, eth_amount): 125 | swap.add_liquidity(initial_amounts, 0, {"from": alice, "value": eth_amount(initial_amounts[0])}) 126 | 127 | 128 | @pytest.fixture 129 | def approve_zap(alice, bob, coins, base_coins, swap, zap): 130 | for token in [*coins, *base_coins, swap]: 131 | token.approve(zap, 2 ** 256 - 1, {"from": alice}) 132 | token.approve(zap, 2 ** 256 - 1, {"from": bob}) 133 | -------------------------------------------------------------------------------- /tests/gauge/test_add_reward.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ZERO_ADDRESS 4 | from brownie_tokens import ERC20 5 | 6 | REWARD = 10 ** 20 7 | WEEK = 7 * 86400 8 | LP_AMOUNT = 10 ** 18 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def initial_setup(gauge, swap, alice, add_initial_liquidity): 13 | swap.approve(gauge, LP_AMOUNT, {"from": alice}) 14 | gauge.deposit(LP_AMOUNT, {"from": alice}) 15 | 16 | 17 | def test_set_rewards_no_deposit(alice, coin_reward, swap, gauge): 18 | gauge.add_reward(coin_reward, alice, {"from": alice}) 19 | 20 | assert swap.balanceOf(gauge) == LP_AMOUNT 21 | assert gauge.reward_tokens(0) == coin_reward 22 | assert gauge.reward_tokens(1) == ZERO_ADDRESS 23 | 24 | 25 | def test_multiple_reward_tokens(alice, coin_reward, coin_a, coin_b, gauge): 26 | for coin in [coin_reward, coin_a, coin_b]: 27 | gauge.add_reward(coin, alice, {"from": alice}) 28 | 29 | assert [coin_reward, coin_a, coin_b] == [gauge.reward_tokens(i) for i in range(3)] 30 | 31 | 32 | def test_cant_exceed_max(alice, gauge): 33 | for _ in range(8): 34 | gauge.add_reward(ERC20(), alice, {"from": alice}) 35 | with brownie.reverts(): 36 | gauge.add_reward(ERC20(), alice, {"from": alice}) 37 | -------------------------------------------------------------------------------- /tests/gauge/test_approve.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize("idx", range(5)) 5 | def test_initial_approval_is_zero(gauge, accounts, idx): 6 | assert gauge.allowance(accounts[0], accounts[idx]) == 0 7 | 8 | 9 | def test_approve(gauge, accounts): 10 | gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 11 | 12 | assert gauge.allowance(accounts[0], accounts[1]) == 10 ** 19 13 | 14 | 15 | def test_modify_approve(gauge, accounts): 16 | gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 17 | gauge.approve(accounts[1], 12345678, {"from": accounts[0]}) 18 | 19 | assert gauge.allowance(accounts[0], accounts[1]) == 12345678 20 | 21 | 22 | def test_revoke_approve(gauge, accounts): 23 | gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 24 | gauge.approve(accounts[1], 0, {"from": accounts[0]}) 25 | 26 | assert gauge.allowance(accounts[0], accounts[1]) == 0 27 | 28 | 29 | def test_approve_self(gauge, accounts): 30 | gauge.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 31 | 32 | assert gauge.allowance(accounts[0], accounts[0]) == 10 ** 19 33 | 34 | 35 | def test_only_affects_target(gauge, accounts): 36 | gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 37 | 38 | assert gauge.allowance(accounts[1], accounts[0]) == 0 39 | 40 | 41 | def test_returns_true(gauge, accounts): 42 | tx = gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 43 | 44 | assert tx.return_value is True 45 | 46 | 47 | def test_approval_event_fires(accounts, gauge): 48 | tx = gauge.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 49 | 50 | assert len(tx.events) == 1 51 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 52 | 53 | 54 | def test_increase_allowance(accounts, gauge): 55 | gauge.approve(accounts[1], 100, {"from": accounts[0]}) 56 | gauge.increaseAllowance(accounts[1], 403, {"from": accounts[0]}) 57 | 58 | assert gauge.allowance(accounts[0], accounts[1]) == 503 59 | 60 | 61 | def test_decrease_allowance(accounts, gauge): 62 | gauge.approve(accounts[1], 100, {"from": accounts[0]}) 63 | gauge.decreaseAllowance(accounts[1], 34, {"from": accounts[0]}) 64 | 65 | assert gauge.allowance(accounts[0], accounts[1]) == 66 66 | -------------------------------------------------------------------------------- /tests/gauge/test_boost_delegation.py: -------------------------------------------------------------------------------- 1 | from brownie import ETH_ADDRESS 2 | 3 | 4 | def test_boost_delegation_not_set_in_proxy(alice, gauge, voting_escrow): 5 | tx = gauge.user_checkpoint(alice, {"from": alice}) 6 | subcall = next( 7 | (tx for tx in tx.subcalls if tx["function"] == "adjusted_balance_of(address)"), {} 8 | ) 9 | next_subcall = tx.subcalls[tx.subcalls.index(subcall) + 1] 10 | 11 | expected = {"to": voting_escrow.address, "op": "STATICCALL", "function": "balanceOf(address)"} 12 | for k in expected.keys(): 13 | assert next_subcall[k] == expected[k] 14 | 15 | 16 | def test_boost_delegation_set_in_addr_provider(alice, gauge, mock_veboost_proxy): 17 | mock_veboost_proxy.set_delegation(ETH_ADDRESS, {"from": alice}) 18 | 19 | tx = gauge.user_checkpoint(alice, {"from": alice}) 20 | subcall = next( 21 | (tx for tx in tx.subcalls if tx["function"] == "adjusted_balance_of(address)"), {} 22 | ) 23 | 24 | assert subcall["return_value"][0] == 69 25 | -------------------------------------------------------------------------------- /tests/gauge/test_checkpoint.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | YEAR = 86400 * 365 4 | 5 | 6 | def test_user_checkpoint(accounts, gauge): 7 | gauge.user_checkpoint(accounts[1], {"from": accounts[1]}) 8 | 9 | 10 | def test_user_checkpoint_new_period(accounts, chain, gauge): 11 | gauge.user_checkpoint(accounts[1], {"from": accounts[1]}) 12 | chain.sleep(int(YEAR * 1.1)) 13 | gauge.user_checkpoint(accounts[1], {"from": accounts[1]}) 14 | 15 | 16 | def test_user_checkpoint_wrong_account(accounts, gauge): 17 | with brownie.reverts("dev: unauthorized"): 18 | gauge.user_checkpoint(accounts[2], {"from": accounts[1]}) 19 | -------------------------------------------------------------------------------- /tests/gauge/test_claim_rewards_multiple.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import brownie 4 | import pytest 5 | from pytest import approx 6 | 7 | REWARD = 10 ** 20 8 | WEEK = 7 * 86400 9 | LP_AMOUNT = 10 ** 18 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def initial_setup(add_initial_liquidity, alice, bob, gauge, coin_reward, coin_a, coin_b, swap): 14 | 15 | # Deposit 16 | swap.transfer(bob, LP_AMOUNT, {"from": alice}) 17 | swap.approve(gauge, LP_AMOUNT, {"from": bob}) 18 | gauge.deposit(LP_AMOUNT, {"from": bob}) 19 | 20 | for coin in [coin_reward, coin_a, coin_b]: 21 | coin._mint_for_testing(alice, REWARD, {"from": alice}) 22 | coin.approve(gauge, 2 ** 256 - 1, {"from": alice}) 23 | gauge.add_reward(coin, alice, {"from": alice}) 24 | gauge.deposit_reward_token(coin, REWARD, {"from": alice}) 25 | 26 | 27 | def test_claim_one_lp(bob, chain, gauge, coin_a, coin_b): 28 | chain.sleep(WEEK) 29 | 30 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 31 | gauge.claim_rewards({"from": bob}) 32 | 33 | for coin in (coin_a, coin_b): 34 | reward = coin.balanceOf(bob) 35 | assert reward <= REWARD 36 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 37 | 38 | 39 | def test_claim_updates_claimed_reward(bob, chain, gauge, coin_a, coin_b): 40 | chain.sleep(WEEK) 41 | 42 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 43 | gauge.claim_rewards({"from": bob}) 44 | 45 | for coin in (coin_a, coin_b): 46 | reward = coin.balanceOf(bob) 47 | assert reward <= REWARD 48 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 49 | assert gauge.claimed_reward(bob, coin) == reward 50 | 51 | 52 | def test_claim_for_other(bob, charlie, chain, gauge, coin_a, coin_b): 53 | chain.sleep(WEEK) 54 | 55 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 56 | gauge.claim_rewards(bob, {"from": charlie}) 57 | 58 | assert coin_a.balanceOf(charlie) == 0 59 | 60 | for coin in (coin_a, coin_b): 61 | reward = coin.balanceOf(bob) 62 | assert reward <= REWARD 63 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 64 | 65 | 66 | def test_claim_for_other_no_reward(bob, charlie, chain, gauge, coin_a, coin_b): 67 | chain.sleep(WEEK) 68 | gauge.claim_rewards(charlie, {"from": bob}) 69 | 70 | assert coin_a.balanceOf(bob) == 0 71 | assert coin_a.balanceOf(charlie) == 0 72 | 73 | assert coin_b.balanceOf(bob) == 0 74 | assert coin_b.balanceOf(charlie) == 0 75 | 76 | 77 | @pytest.mark.no_call_coverage 78 | def test_claim_two_lp(alice, bob, chain, gauge, swap, coin_a, coin_b): 79 | 80 | chain.sleep(86400) 81 | # Deposit 82 | swap.approve(gauge, LP_AMOUNT, {"from": alice}) 83 | gauge.deposit(LP_AMOUNT, {"from": alice}) 84 | 85 | chain.sleep(WEEK) 86 | chain.mine() 87 | 88 | for acct in (alice, bob): 89 | gauge.claim_rewards({"from": acct}) 90 | 91 | for coin in (coin_a, coin_b): 92 | # Calculate rewards 93 | assert coin.balanceOf(bob) > coin.balanceOf(alice) > 0 94 | assert math.isclose((coin.balanceOf(gauge) / REWARD), 0, abs_tol=10 ** -9) 95 | 96 | 97 | def test_claim_set_alt_receiver(bob, charlie, chain, gauge, coin_a, coin_b): 98 | chain.sleep(WEEK) 99 | 100 | gauge.claim_rewards(bob, charlie, {"from": bob}) 101 | 102 | assert coin_a.balanceOf(bob) == 0 103 | assert coin_b.balanceOf(bob) == 0 104 | 105 | for coin in (coin_a, coin_b): 106 | reward = coin.balanceOf(charlie) 107 | assert reward <= REWARD 108 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 109 | 110 | 111 | def test_claim_for_other_changing_receiver_reverts(bob, charlie, chain, gauge): 112 | chain.sleep(WEEK) 113 | 114 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 115 | with brownie.reverts("dev: cannot redirect when claiming for another user"): 116 | gauge.claim_rewards(bob, charlie, {"from": charlie}) 117 | -------------------------------------------------------------------------------- /tests/gauge/test_claim_rewards_none.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | REWARD = 10 ** 20 4 | WEEK = 7 * 86400 5 | LP_AMOUNT = 10 ** 18 6 | 7 | 8 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 9 | 10 | 11 | def test_claim_no_deposit(alice, bob, chain, gauge, swap, coin_reward): 12 | # Fund 13 | swap.approve(gauge, LP_AMOUNT, {"from": alice}) 14 | gauge.deposit(LP_AMOUNT, {"from": alice}) 15 | 16 | coin_reward._mint_for_testing(alice, REWARD, {"from": alice}) 17 | coin_reward.approve(gauge, REWARD, {"from": alice}) 18 | gauge.add_reward(coin_reward, alice, {"from": alice}) 19 | gauge.deposit_reward_token(coin_reward, REWARD, {"from": alice}) 20 | 21 | chain.sleep(WEEK) 22 | 23 | gauge.claim_rewards({"from": bob}) 24 | 25 | assert coin_reward.balanceOf(bob) == 0 26 | 27 | 28 | def test_claim_no_rewards(alice, bob, chain, gauge, swap, coin_reward): 29 | # Deposit 30 | swap.transfer(bob, LP_AMOUNT, {"from": alice}) 31 | swap.approve(gauge, LP_AMOUNT, {"from": bob}) 32 | gauge.deposit(LP_AMOUNT, {"from": bob}) 33 | 34 | gauge.add_reward(coin_reward, alice, {"from": alice}) 35 | 36 | chain.sleep(WEEK) 37 | 38 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 39 | gauge.claim_rewards({"from": bob}) 40 | 41 | assert coin_reward.balanceOf(bob) == 0 42 | -------------------------------------------------------------------------------- /tests/gauge/test_claim_rewards_transfer.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pytest 4 | 5 | REWARD = 10 ** 20 6 | WEEK = 7 * 86400 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def initial_setup( 11 | add_initial_liquidity, 12 | alice, 13 | chain, 14 | coin_reward, 15 | swap, 16 | gauge, 17 | gauge_controller, 18 | ): 19 | # gauge setup 20 | gauge_controller.add_type(b"Liquidity", 10 ** 10, {"from": alice}) 21 | gauge_controller.add_gauge(gauge, 0, 0, {"from": alice}) 22 | 23 | # deposit into gauge 24 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 25 | gauge.deposit(10 ** 18, {"from": alice}) 26 | 27 | # add rewards 28 | gauge.add_reward(coin_reward, alice, {"from": alice}) 29 | 30 | # fund rewards 31 | coin_reward._mint_for_testing(alice, REWARD, {"from": alice}) 32 | coin_reward.approve(gauge, REWARD, {"from": alice}) 33 | gauge.deposit_reward_token(coin_reward, REWARD, {"from": alice}) 34 | 35 | # sleep half way through the reward period 36 | chain.sleep(int(86400 * 3.5)) 37 | 38 | 39 | def test_transfer_does_not_trigger_claim_for_sender(alice, bob, gauge, coin_reward): 40 | amount = gauge.balanceOf(alice) 41 | 42 | gauge.transfer(bob, amount, {"from": alice}) 43 | 44 | reward = coin_reward.balanceOf(alice) 45 | assert reward == 0 46 | 47 | 48 | def test_transfer_does_not_trigger_claim_for_receiver(alice, bob, chain, gauge, coin_reward): 49 | amount = gauge.balanceOf(alice) // 2 50 | 51 | gauge.transfer(bob, amount, {"from": alice}) 52 | chain.sleep(WEEK) 53 | gauge.transfer(alice, amount, {"from": bob}) 54 | 55 | for acct in (alice, bob): 56 | assert coin_reward.balanceOf(acct) == 0 57 | 58 | 59 | def test_claim_rewards_stil_accurate(alice, bob, chain, gauge, coin_reward): 60 | amount = gauge.balanceOf(alice) 61 | 62 | gauge.transfer(bob, amount, {"from": alice}) 63 | 64 | # sleep half way through the reward period 65 | chain.sleep(int(86400 * 3.5)) 66 | 67 | for acct in (alice, bob): 68 | gauge.claim_rewards({"from": acct}) 69 | 70 | assert math.isclose(coin_reward.balanceOf(acct), REWARD // 2, rel_tol=0.01) 71 | -------------------------------------------------------------------------------- /tests/gauge/test_claim_rewards_unipool.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import approx 3 | 4 | REWARD = 10 ** 20 5 | WEEK = 7 * 86400 6 | LP_AMOUNT = 10 ** 18 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def initial_setup( 11 | add_initial_liquidity, 12 | alice, 13 | bob, 14 | coin_reward, 15 | swap, 16 | gauge, 17 | gauge_controller, 18 | ): 19 | # gauge setup 20 | gauge_controller.add_type(b"Liquidity", 10 ** 10, {"from": alice}) 21 | gauge_controller.add_gauge(gauge, 0, 0, {"from": alice}) 22 | 23 | # deposit into gauge 24 | swap.transfer(bob, LP_AMOUNT, {"from": alice}) 25 | swap.approve(gauge, LP_AMOUNT, {"from": bob}) 26 | gauge.deposit(LP_AMOUNT, {"from": bob}) 27 | 28 | # add rewards 29 | gauge.add_reward(coin_reward, alice, {"from": alice}) 30 | 31 | # fund rewards 32 | coin_reward._mint_for_testing(alice, REWARD, {"from": alice}) 33 | coin_reward.approve(gauge, REWARD, {"from": alice}) 34 | gauge.deposit_reward_token(coin_reward, REWARD, {"from": alice}) 35 | 36 | 37 | def test_claim_one_lp(bob, chain, gauge, coin_reward): 38 | chain.sleep(WEEK) 39 | 40 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 41 | gauge.claim_rewards({"from": bob}) 42 | 43 | reward = coin_reward.balanceOf(bob) 44 | assert reward <= REWARD 45 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 46 | 47 | 48 | def test_claim_for_other(bob, charlie, chain, gauge, coin_reward): 49 | chain.sleep(WEEK) 50 | 51 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 52 | gauge.claim_rewards(bob, {"from": charlie}) 53 | 54 | assert coin_reward.balanceOf(charlie) == 0 55 | 56 | reward = coin_reward.balanceOf(bob) 57 | assert reward <= REWARD 58 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 59 | 60 | 61 | def test_claim_for_other_no_reward(bob, charlie, chain, gauge, coin_reward): 62 | chain.sleep(WEEK) 63 | gauge.claim_rewards(charlie, {"from": bob}) 64 | 65 | assert coin_reward.balanceOf(bob) == 0 66 | assert coin_reward.balanceOf(charlie) == 0 67 | 68 | 69 | def test_claim_two_lp(alice, bob, chain, gauge, swap, coin_reward): 70 | 71 | # Deposit 72 | swap.approve(gauge, LP_AMOUNT, {"from": alice}) 73 | gauge.deposit(LP_AMOUNT, {"from": alice}) 74 | 75 | chain.sleep(WEEK) 76 | chain.mine() 77 | 78 | # Calculate rewards 79 | claimable_rewards = [ 80 | gauge.claimable_reward.call(acc, coin_reward, {"from": acc}) for acc in (alice, bob) 81 | ] 82 | 83 | # Claim rewards 84 | rewards = [] 85 | for acct in (alice, bob): 86 | gauge.claim_rewards({"from": acct}) 87 | rewards += [coin_reward.balanceOf(acct)] 88 | 89 | # Calculation == results 90 | assert tuple(claimable_rewards) == tuple(rewards) 91 | 92 | # Approximately equal apart from what caused by 1 s ganache-cli jitter 93 | assert sum(rewards) <= REWARD 94 | assert approx(sum(rewards), REWARD, 1.001 / WEEK) # ganache-cli jitter of 1 s 95 | assert approx(rewards[0], rewards[1], 2.002 * WEEK) 96 | -------------------------------------------------------------------------------- /tests/gauge/test_deposit_for.py: -------------------------------------------------------------------------------- 1 | from math import isclose 2 | 3 | import pytest 4 | 5 | REWARD = 10 ** 20 6 | WEEK = 7 * 86400 7 | LP_AMOUNT = 10 ** 18 8 | 9 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 10 | 11 | 12 | def test_no_approval_needed(alice, bob, gauge, swap): 13 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 14 | gauge.deposit(100_000, bob, {"from": alice}) 15 | 16 | assert gauge.balanceOf(bob) == 100_000 17 | 18 | 19 | def test_deposit_for_and_claim_rewards(alice, bob, chain, gauge, swap, coin_reward): 20 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 21 | gauge.deposit(LP_AMOUNT, bob, {"from": alice}) 22 | 23 | gauge.add_reward(coin_reward, alice, {"from": alice}) 24 | 25 | coin_reward._mint_for_testing(alice, REWARD, {"from": alice}) 26 | coin_reward.approve(gauge, 2 ** 256 - 1, {"from": alice}) 27 | gauge.deposit_reward_token(coin_reward, REWARD, {"from": alice}) 28 | 29 | chain.mine(timedelta=WEEK) 30 | 31 | # alice deposits for bob and claims rewards for him 32 | gauge.deposit(LP_AMOUNT, bob, True, {"from": alice}) 33 | 34 | assert isclose(REWARD, coin_reward.balanceOf(bob), rel_tol=0.001) 35 | -------------------------------------------------------------------------------- /tests/gauge/test_deposit_withdraw.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | 5 | @pytest.fixture(autouse=True) 6 | def deposit_setup(alice, add_initial_liquidity, gauge, swap): 7 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 8 | 9 | 10 | def test_deposit(alice, gauge, swap): 11 | balance = swap.balanceOf(alice) 12 | gauge.deposit(100000, {"from": alice}) 13 | 14 | assert swap.balanceOf(gauge) == 100000 15 | assert swap.balanceOf(alice) == balance - 100000 16 | assert gauge.totalSupply() == 100000 17 | assert gauge.balanceOf(alice) == 100000 18 | 19 | 20 | def test_deposit_zero(alice, gauge, swap): 21 | balance = swap.balanceOf(alice) 22 | gauge.deposit(0, {"from": alice}) 23 | 24 | assert swap.balanceOf(gauge) == 0 25 | assert swap.balanceOf(alice) == balance 26 | assert gauge.totalSupply() == 0 27 | assert gauge.balanceOf(alice) == 0 28 | 29 | 30 | def test_deposit_insufficient_balance(bob, gauge): 31 | with brownie.reverts(): 32 | gauge.deposit(100000, {"from": bob}) 33 | 34 | 35 | def test_withdraw(alice, gauge, swap): 36 | balance = swap.balanceOf(alice) 37 | 38 | gauge.deposit(100000, {"from": alice}) 39 | gauge.withdraw(100000, {"from": alice}) 40 | 41 | assert swap.balanceOf(gauge) == 0 42 | assert swap.balanceOf(alice) == balance 43 | assert gauge.totalSupply() == 0 44 | assert gauge.balanceOf(alice) == 0 45 | 46 | 47 | def test_withdraw_zero(alice, gauge, swap): 48 | balance = swap.balanceOf(alice) 49 | gauge.deposit(100000, {"from": alice}) 50 | gauge.withdraw(0, {"from": alice}) 51 | 52 | assert swap.balanceOf(gauge) == 100000 53 | assert swap.balanceOf(alice) == balance - 100000 54 | assert gauge.totalSupply() == 100000 55 | assert gauge.balanceOf(alice) == 100000 56 | 57 | 58 | def test_withdraw_new_epoch(alice, chain, gauge, swap): 59 | balance = swap.balanceOf(alice) 60 | 61 | gauge.deposit(100000, {"from": alice}) 62 | chain.sleep(86400 * 400) 63 | gauge.withdraw(100000, {"from": alice}) 64 | 65 | assert swap.balanceOf(gauge) == 0 66 | assert swap.balanceOf(alice) == balance 67 | assert gauge.totalSupply() == 0 68 | assert gauge.balanceOf(alice) == 0 69 | -------------------------------------------------------------------------------- /tests/gauge/test_kick.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | MAX_UINT256 = 2 ** 256 - 1 4 | WEEK = 7 * 86400 5 | 6 | 7 | def test_kick(add_initial_liquidity, alice, bob, chain, gauge, voting_escrow, crv, swap): 8 | chain.sleep(2 * WEEK + 5) 9 | 10 | crv.approve(voting_escrow, MAX_UINT256, {"from": alice}) 11 | voting_escrow.create_lock(10 ** 20, chain.time() + 4 * WEEK, {"from": alice}) 12 | 13 | swap.approve(gauge.address, MAX_UINT256, {"from": alice}) 14 | gauge.deposit(10 ** 21, {"from": alice}) 15 | 16 | assert gauge.working_balances(alice) == 10 ** 21 17 | 18 | chain.sleep(WEEK) 19 | 20 | with brownie.reverts("dev: kick not allowed"): 21 | gauge.kick(alice, {"from": bob}) 22 | 23 | chain.sleep(4 * WEEK) 24 | 25 | gauge.kick(alice, {"from": bob}) 26 | assert gauge.working_balances(alice) == 4 * 10 ** 20 27 | 28 | with brownie.reverts("dev: kick not needed"): 29 | gauge.kick(alice, {"from": bob}) 30 | -------------------------------------------------------------------------------- /tests/gauge/test_mass_exit_reward_claim.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import pytest 4 | 5 | REWARD = 10 ** 20 6 | WEEK = 7 * 86400 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def initial_setup( 11 | add_initial_liquidity, 12 | alice, 13 | accounts, 14 | chain, 15 | coin_reward, 16 | swap, 17 | gauge, 18 | gauge_controller, 19 | ): 20 | # gauge setup 21 | gauge_controller.add_type(b"Liquidity", 10 ** 10, {"from": alice}) 22 | gauge_controller.add_gauge(gauge, 0, 0, {"from": alice}) 23 | 24 | # deposit into gauge 25 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 26 | 27 | for acct in accounts[:10]: 28 | gauge.deposit(10 ** 18, acct, {"from": alice}) 29 | 30 | # add rewards 31 | gauge.add_reward(coin_reward, alice, {"from": alice}) 32 | 33 | # fund rewards 34 | coin_reward._mint_for_testing(alice, REWARD, {"from": alice}) 35 | coin_reward.approve(gauge, 2 ** 256 - 1, {"from": alice}) 36 | gauge.deposit_reward_token(coin_reward, REWARD, {"from": alice}) 37 | 38 | # sleep half way through the reward period 39 | chain.sleep(WEEK) 40 | 41 | 42 | def test_mass_withdraw_claim_rewards(accounts, gauge, coin_reward): 43 | for account in accounts[:10]: 44 | gauge.withdraw(gauge.balanceOf(account), {"from": account}) 45 | assert gauge.claimed_reward(account, coin_reward) == 0 46 | 47 | for account in accounts[:10]: 48 | gauge.claim_rewards({"from": account}) 49 | assert math.isclose(coin_reward.balanceOf(account), REWARD / 10) 50 | -------------------------------------------------------------------------------- /tests/gauge/test_set_rewards_receiver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import approx 3 | 4 | REWARD = 10 ** 20 5 | WEEK = 7 * 86400 6 | LP_AMOUNT = 10 ** 18 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def initial_setup( 11 | alice, add_initial_liquidity, bob, chain, gauge, coin_reward, coin_a, coin_b, swap 12 | ): 13 | 14 | for coin in [coin_reward, coin_a, coin_b]: 15 | gauge.add_reward(coin, alice, {"from": alice}) 16 | coin._mint_for_testing(alice, REWARD, {"from": alice}) 17 | coin.approve(gauge, 2 ** 256 - 1, {"from": alice}) 18 | gauge.deposit_reward_token(coin, REWARD, {"from": alice}) 19 | 20 | # Deposit 21 | swap.transfer(bob, LP_AMOUNT, {"from": alice}) 22 | swap.approve(gauge, LP_AMOUNT, {"from": bob}) 23 | gauge.deposit(LP_AMOUNT, {"from": bob}) 24 | 25 | chain.sleep(WEEK) 26 | 27 | 28 | def test_claim_one_lp(alice, bob, chain, gauge, coin_a, coin_b): 29 | chain.sleep(WEEK) 30 | 31 | gauge.set_rewards_receiver(alice, {"from": bob}) 32 | 33 | gauge.withdraw(LP_AMOUNT, {"from": bob}) 34 | gauge.claim_rewards({"from": bob}) 35 | 36 | for coin in (coin_a, coin_b): 37 | reward = coin.balanceOf(alice) 38 | assert reward <= REWARD 39 | assert approx(REWARD, reward, 1.001 / WEEK) # ganache-cli jitter of 1 s 40 | -------------------------------------------------------------------------------- /tests/gauge/test_transfer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup(add_initial_liquidity, alice, gauge_controller, minter, gauge, swap): 8 | 9 | gauge_controller.add_type(b"Liquidity", 10 ** 10, {"from": alice}) 10 | gauge_controller.add_gauge(gauge, 0, 0, {"from": alice}) 11 | 12 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 13 | gauge.deposit(10 ** 18, {"from": alice}) 14 | 15 | 16 | def test_sender_balance_decreases(alice, bob, gauge): 17 | sender_balance = gauge.balanceOf(alice) 18 | amount = sender_balance // 4 19 | 20 | gauge.transfer(bob, amount, {"from": alice}) 21 | 22 | assert gauge.balanceOf(alice) == sender_balance - amount 23 | 24 | 25 | def test_receiver_balance_increases(alice, bob, gauge): 26 | receiver_balance = gauge.balanceOf(bob) 27 | amount = gauge.balanceOf(alice) // 4 28 | 29 | gauge.transfer(bob, amount, {"from": alice}) 30 | 31 | assert gauge.balanceOf(bob) == receiver_balance + amount 32 | 33 | 34 | def test_total_supply_not_affected(alice, bob, gauge): 35 | total_supply = gauge.totalSupply() 36 | amount = gauge.balanceOf(alice) 37 | 38 | gauge.transfer(bob, amount, {"from": alice}) 39 | 40 | assert gauge.totalSupply() == total_supply 41 | 42 | 43 | def test_returns_true(alice, bob, gauge): 44 | amount = gauge.balanceOf(alice) 45 | tx = gauge.transfer(bob, amount, {"from": alice}) 46 | 47 | assert tx.return_value is True 48 | 49 | 50 | def test_transfer_full_balance(alice, bob, gauge): 51 | amount = gauge.balanceOf(alice) 52 | receiver_balance = gauge.balanceOf(bob) 53 | 54 | gauge.transfer(bob, amount, {"from": alice}) 55 | 56 | assert gauge.balanceOf(alice) == 0 57 | assert gauge.balanceOf(bob) == receiver_balance + amount 58 | 59 | 60 | def test_transfer_zero_tokens(alice, bob, gauge): 61 | sender_balance = gauge.balanceOf(alice) 62 | receiver_balance = gauge.balanceOf(bob) 63 | 64 | gauge.transfer(bob, 0, {"from": alice}) 65 | 66 | assert gauge.balanceOf(alice) == sender_balance 67 | assert gauge.balanceOf(bob) == receiver_balance 68 | 69 | 70 | def test_transfer_to_self(alice, gauge): 71 | sender_balance = gauge.balanceOf(alice) 72 | amount = sender_balance // 4 73 | 74 | gauge.transfer(alice, amount, {"from": alice}) 75 | 76 | assert gauge.balanceOf(alice) == sender_balance 77 | 78 | 79 | def test_insufficient_balance(alice, bob, gauge): 80 | balance = gauge.balanceOf(alice) 81 | 82 | with brownie.reverts(): 83 | gauge.transfer(bob, balance + 1, {"from": alice}) 84 | 85 | 86 | def test_transfer_event_fires(alice, bob, gauge): 87 | amount = gauge.balanceOf(alice) 88 | tx = gauge.transfer(bob, amount, {"from": alice}) 89 | 90 | assert tx.events["Transfer"].values() == [alice, bob, amount] 91 | -------------------------------------------------------------------------------- /tests/gauge/test_transferFrom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup(alice, add_initial_liquidity, gauge_controller, gauge, swap): 8 | 9 | gauge_controller.add_type(b"Liquidity", 10 ** 10, {"from": alice}) 10 | gauge_controller.add_gauge(gauge, 0, 0, {"from": alice}) 11 | 12 | swap.approve(gauge, 2 ** 256 - 1, {"from": alice}) 13 | gauge.deposit(10 ** 18, {"from": alice}) 14 | 15 | 16 | def test_sender_balance_decreases(alice, bob, charlie, gauge): 17 | sender_balance = gauge.balanceOf(alice) 18 | amount = sender_balance // 4 19 | 20 | gauge.approve(bob, amount, {"from": alice}) 21 | gauge.transferFrom(alice, charlie, amount, {"from": bob}) 22 | 23 | assert gauge.balanceOf(alice) == sender_balance - amount 24 | 25 | 26 | def test_receiver_balance_increases(alice, bob, charlie, gauge): 27 | receiver_balance = gauge.balanceOf(charlie) 28 | amount = gauge.balanceOf(alice) // 4 29 | 30 | gauge.approve(bob, amount, {"from": alice}) 31 | gauge.transferFrom(alice, charlie, amount, {"from": bob}) 32 | 33 | assert gauge.balanceOf(charlie) == receiver_balance + amount 34 | 35 | 36 | def test_caller_balance_not_affected(alice, bob, charlie, gauge): 37 | caller_balance = gauge.balanceOf(bob) 38 | amount = gauge.balanceOf(alice) 39 | 40 | gauge.approve(bob, amount, {"from": alice}) 41 | gauge.transferFrom(alice, charlie, amount, {"from": bob}) 42 | 43 | assert gauge.balanceOf(bob) == caller_balance 44 | 45 | 46 | def test_caller_approval_affected(alice, bob, charlie, gauge): 47 | approval_amount = gauge.balanceOf(alice) 48 | transfer_amount = approval_amount // 4 49 | 50 | gauge.approve(bob, approval_amount, {"from": alice}) 51 | gauge.transferFrom(alice, charlie, transfer_amount, {"from": bob}) 52 | 53 | assert gauge.allowance(alice, bob) == approval_amount - transfer_amount 54 | 55 | 56 | def test_receiver_approval_not_affected(alice, bob, charlie, gauge): 57 | approval_amount = gauge.balanceOf(alice) 58 | transfer_amount = approval_amount // 4 59 | 60 | gauge.approve(bob, approval_amount, {"from": alice}) 61 | gauge.approve(charlie, approval_amount, {"from": alice}) 62 | gauge.transferFrom(alice, charlie, transfer_amount, {"from": bob}) 63 | 64 | assert gauge.allowance(alice, charlie) == approval_amount 65 | 66 | 67 | def test_total_supply_not_affected(alice, bob, charlie, gauge): 68 | total_supply = gauge.totalSupply() 69 | amount = gauge.balanceOf(alice) 70 | 71 | gauge.approve(bob, amount, {"from": alice}) 72 | gauge.transferFrom(alice, charlie, amount, {"from": bob}) 73 | 74 | assert gauge.totalSupply() == total_supply 75 | 76 | 77 | def test_returns_true(alice, bob, charlie, gauge): 78 | amount = gauge.balanceOf(alice) 79 | gauge.approve(bob, amount, {"from": alice}) 80 | tx = gauge.transferFrom(alice, charlie, amount, {"from": bob}) 81 | 82 | assert tx.return_value is True 83 | 84 | 85 | def test_transfer_full_balance(alice, bob, charlie, gauge): 86 | amount = gauge.balanceOf(alice) 87 | receiver_balance = gauge.balanceOf(charlie) 88 | 89 | gauge.approve(bob, amount, {"from": alice}) 90 | gauge.transferFrom(alice, charlie, amount, {"from": bob}) 91 | 92 | assert gauge.balanceOf(alice) == 0 93 | assert gauge.balanceOf(charlie) == receiver_balance + amount 94 | 95 | 96 | def test_transfer_zero_tokens(alice, bob, charlie, gauge): 97 | sender_balance = gauge.balanceOf(alice) 98 | receiver_balance = gauge.balanceOf(charlie) 99 | 100 | gauge.approve(bob, sender_balance, {"from": alice}) 101 | gauge.transferFrom(alice, charlie, 0, {"from": bob}) 102 | 103 | assert gauge.balanceOf(alice) == sender_balance 104 | assert gauge.balanceOf(charlie) == receiver_balance 105 | 106 | 107 | def test_transfer_zero_tokens_without_approval(alice, bob, charlie, gauge): 108 | sender_balance = gauge.balanceOf(alice) 109 | receiver_balance = gauge.balanceOf(charlie) 110 | 111 | gauge.transferFrom(alice, charlie, 0, {"from": bob}) 112 | 113 | assert gauge.balanceOf(alice) == sender_balance 114 | assert gauge.balanceOf(charlie) == receiver_balance 115 | 116 | 117 | def test_insufficient_balance(alice, bob, charlie, gauge): 118 | balance = gauge.balanceOf(alice) 119 | 120 | gauge.approve(bob, balance + 1, {"from": alice}) 121 | with brownie.reverts(): 122 | gauge.transferFrom(alice, charlie, balance + 1, {"from": bob}) 123 | 124 | 125 | def test_insufficient_approval(alice, bob, charlie, gauge): 126 | balance = gauge.balanceOf(alice) 127 | 128 | gauge.approve(bob, balance - 1, {"from": alice}) 129 | with brownie.reverts(): 130 | gauge.transferFrom(alice, charlie, balance, {"from": bob}) 131 | 132 | 133 | def test_no_approval(alice, bob, charlie, gauge): 134 | balance = gauge.balanceOf(alice) 135 | 136 | with brownie.reverts(): 137 | gauge.transferFrom(alice, charlie, balance, {"from": bob}) 138 | 139 | 140 | def test_infinite_approval(alice, bob, charlie, gauge): 141 | gauge.approve(bob, 2 ** 256 - 1, {"from": alice}) 142 | gauge.transferFrom(alice, charlie, 10000, {"from": bob}) 143 | 144 | assert gauge.allowance(alice, bob) == 2 ** 256 - 1 145 | 146 | 147 | def test_revoked_approval(alice, bob, charlie, gauge): 148 | balance = gauge.balanceOf(alice) 149 | 150 | gauge.approve(bob, balance, {"from": alice}) 151 | gauge.approve(bob, 0, {"from": alice}) 152 | 153 | with brownie.reverts(): 154 | gauge.transferFrom(alice, charlie, balance, {"from": bob}) 155 | 156 | 157 | def test_transfer_to_self(alice, gauge): 158 | sender_balance = gauge.balanceOf(alice) 159 | amount = sender_balance // 4 160 | 161 | gauge.approve(alice, sender_balance, {"from": alice}) 162 | gauge.transferFrom(alice, alice, amount, {"from": alice}) 163 | 164 | assert gauge.balanceOf(alice) == sender_balance 165 | assert gauge.allowance(alice, alice) == sender_balance - amount 166 | 167 | 168 | def test_transfer_to_self_no_approval(alice, gauge): 169 | amount = gauge.balanceOf(alice) 170 | 171 | with brownie.reverts(): 172 | gauge.transferFrom(alice, alice, amount, {"from": alice}) 173 | 174 | 175 | def test_transfer_event_fires(alice, bob, charlie, gauge): 176 | amount = gauge.balanceOf(alice) 177 | 178 | gauge.approve(bob, amount, {"from": alice}) 179 | tx = gauge.transferFrom(alice, charlie, amount, {"from": bob}) 180 | 181 | assert tx.events["Transfer"].values() == [alice, charlie, amount] 182 | -------------------------------------------------------------------------------- /tests/pools/common/test_add_liquidity.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob", "approve_bob") 6 | 7 | 8 | def test_add_liquidity(bob, swap, coins, initial_amounts, eth_amount): 9 | swap.add_liquidity(initial_amounts, 0, {"from": bob, "value": eth_amount(initial_amounts[0])}) 10 | 11 | for coin, amount in zip(coins, initial_amounts): 12 | if coin == ETH_ADDRESS: 13 | bob.balance() == 0 14 | swap.balance() == amount * 2 15 | continue 16 | 17 | assert coin.balanceOf(bob) == 0 18 | assert coin.balanceOf(swap) == amount * 2 19 | 20 | ideal = len(coins) * 1_000_000 * 10 ** 18 21 | assert abs(swap.balanceOf(bob) - ideal) <= 1 22 | assert abs(swap.totalSupply() - ideal * 2) <= 2 23 | 24 | 25 | @pytest.mark.parametrize("idx", range(2)) 26 | def test_add_one_coin(bob, swap, coins, initial_amounts, idx, eth_amount): 27 | amounts = [0] * len(coins) 28 | amounts[idx] = initial_amounts[idx] 29 | 30 | swap.add_liquidity(amounts, 0, {"from": bob, "value": eth_amount(amounts[0])}) 31 | 32 | for i, coin in enumerate(coins): 33 | if coin == ETH_ADDRESS: 34 | bob.balance() == initial_amounts[0] - amounts[0] 35 | swap.balance() == initial_amounts[0] + amounts[0] 36 | continue 37 | 38 | assert coin.balanceOf(bob) == initial_amounts[i] - amounts[i] 39 | assert coin.balanceOf(swap) == initial_amounts[i] + amounts[i] 40 | 41 | difference = abs(swap.balanceOf(bob) - (10 ** 18 * 1_000_000)) 42 | assert difference / (10 ** 18 * 1_000_000) < 0.01 43 | 44 | 45 | def test_insufficient_balance(charlie, swap, decimals): 46 | amounts = [(10 ** i) for i in decimals] 47 | 48 | with brownie.reverts(): # invalid approval or balance 49 | swap.add_liquidity(amounts, 0, {"from": charlie}) 50 | 51 | 52 | def test_min_amount_too_high(bob, swap, initial_amounts, plain_pool_size, eth_amount): 53 | with brownie.reverts(): 54 | swap.add_liquidity( 55 | initial_amounts, 56 | plain_pool_size * 1_000_000 * 10 ** 18 + 1, 57 | {"from": bob, "value": eth_amount(initial_amounts[0])}, 58 | ) 59 | 60 | 61 | def test_event(bob, swap, initial_amounts, eth_amount): 62 | tx = swap.add_liquidity( 63 | initial_amounts, 0, {"from": bob, "value": eth_amount(initial_amounts[0])} 64 | ) 65 | 66 | event = tx.events["AddLiquidity"] 67 | assert event["provider"] == bob 68 | assert event["token_amounts"] == initial_amounts 69 | assert event["token_supply"] == swap.totalSupply() 70 | 71 | 72 | def test_send_eth(bob, swap, initial_amounts): 73 | with brownie.reverts(): 74 | swap.add_liquidity(initial_amounts, 0, {"from": bob, "value": 1}) 75 | -------------------------------------------------------------------------------- /tests/pools/common/test_add_liquidity_initial.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("mint_alice", "approve_alice") 6 | 7 | 8 | @pytest.mark.parametrize("min_amount", [0, 10 ** 18]) 9 | def test_initial(alice, swap, coins, min_amount, decimals, initial_amounts, eth_amount): 10 | amounts = [10 ** i for i in decimals] 11 | 12 | swap.add_liquidity( 13 | amounts, 14 | len(coins) * min_amount, 15 | {"from": alice, "value": eth_amount(amounts[0])}, 16 | ) 17 | 18 | for coin, amount, initial in zip(coins, amounts, initial_amounts): 19 | if coin == ETH_ADDRESS: 20 | assert alice.balance() == initial - amount 21 | assert swap.balance() == amount 22 | continue 23 | 24 | assert coin.balanceOf(alice) == initial - amount 25 | assert coin.balanceOf(swap) == amount 26 | 27 | 28 | @pytest.mark.parametrize("idx", range(2)) 29 | def test_initial_liquidity_missing_coin(alice, swap, idx, decimals, eth_amount): 30 | amounts = [10 ** i for i in decimals] 31 | amounts[idx] = 0 32 | with brownie.reverts(): 33 | swap.add_liquidity(amounts, 0, {"from": alice, "value": eth_amount(amounts[0])}) 34 | -------------------------------------------------------------------------------- /tests/pools/common/test_claim_fees.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob", "approve_bob") 5 | 6 | 7 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 8 | def test_admin_balances(bob, swap, coins, initial_amounts, sending, receiving, eth_amount): 9 | for send, recv in [(sending, receiving), (receiving, sending)]: 10 | swap.exchange( 11 | send, 12 | recv, 13 | initial_amounts[send], 14 | 0, 15 | {"from": bob, "value": eth_amount(initial_amounts[send]) if send == 0 else 0}, 16 | ) 17 | 18 | for i in (sending, receiving): 19 | if coins[0] == ETH_ADDRESS and i == 0: 20 | admin_fee = swap.balance() - swap.balances(0) 21 | assert admin_fee + swap.balances(0) == swap.balance() 22 | else: 23 | admin_fee = coins[i].balanceOf(swap) - swap.balances(i) 24 | assert admin_fee + swap.balances(i) == coins[i].balanceOf(swap) 25 | 26 | assert admin_fee > 0 27 | 28 | 29 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 30 | def test_withdraw_one_coin( 31 | alice, bob, swap, coins, sending, receiving, initial_amounts, eth_amount 32 | ): 33 | 34 | swap.exchange( 35 | sending, 36 | receiving, 37 | initial_amounts[sending], 38 | 0, 39 | {"from": bob, "value": eth_amount(initial_amounts[sending]) if sending == 0 else 0}, 40 | ) 41 | 42 | admin_balance = swap.admin_balances(receiving) 43 | 44 | assert admin_balance > 0 45 | assert swap.admin_balances(sending) == 0 46 | 47 | swap.withdraw_admin_fees({"from": alice}) 48 | 49 | if coins[0] == ETH_ADDRESS and receiving == 0: 50 | assert swap.balances(receiving) == swap.balance() 51 | else: 52 | assert swap.balances(receiving) == coins[receiving].balanceOf(swap) 53 | 54 | 55 | def test_no_fees(bob, fee_receiver, swap, coins): 56 | pre_eth_balance = fee_receiver.balance() 57 | swap.withdraw_admin_fees({"from": bob}) 58 | 59 | for coin in coins: 60 | if coin == ETH_ADDRESS: 61 | assert fee_receiver.balance() == pre_eth_balance 62 | continue 63 | assert coin.balanceOf(fee_receiver) == 0 64 | -------------------------------------------------------------------------------- /tests/pools/common/test_exchange.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob") 5 | 6 | 7 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 8 | def test_min_dy(bob, swap, coins, sending, receiving, decimals, eth_amount): 9 | amount = 10 ** decimals[sending] 10 | if coins[sending] != ETH_ADDRESS: 11 | coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 12 | 13 | min_dy = swap.get_dy(sending, receiving, amount) 14 | pre_bal = bob.balance() 15 | swap.exchange( 16 | sending, 17 | receiving, 18 | amount, 19 | min_dy - 1, 20 | {"from": bob, "value": eth_amount(amount) if sending == 0 else 0}, 21 | ) 22 | 23 | if coins[0] == ETH_ADDRESS and receiving == 0: 24 | received = bob.balance() - pre_bal 25 | else: 26 | received = coins[receiving].balanceOf(bob) 27 | assert abs(received - min_dy) <= 1 28 | -------------------------------------------------------------------------------- /tests/pools/common/test_exchange_imbalanced.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob") 5 | 6 | 7 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 8 | def test_min_dy(accounts, bob, swap, coins, sending, receiving, decimals, eth_amount, is_eth_pool): 9 | 10 | amounts = [10 ** i for i in decimals] 11 | scaler = amounts.copy() # used to scale token amounts when decimals are different 12 | 13 | amounts[sending] = 0 14 | amounts[receiving] = amounts[receiving] * 10 ** 8 15 | 16 | for i, amount in enumerate(amounts): 17 | if coins[i] == ETH_ADDRESS: 18 | accounts[-1].transfer(bob, amount) 19 | continue 20 | coins[i]._mint_for_testing(bob, amount, {"from": bob}) 21 | 22 | swap.add_liquidity(amounts, 0, {"from": bob, "value": eth_amount(amounts[0])}) 23 | 24 | if sending == 0 and is_eth_pool: 25 | pass 26 | else: 27 | coins[sending]._mint_for_testing(bob, scaler[sending], {"from": bob}) 28 | 29 | # we need to scale these appropriately for tokens with different decimal values 30 | min_dy_sending = swap.get_dy(sending, receiving, scaler[sending]) / scaler[receiving] 31 | min_dy_receiving = swap.get_dy(receiving, sending, scaler[receiving]) / scaler[sending] 32 | 33 | assert min_dy_sending > min_dy_receiving 34 | -------------------------------------------------------------------------------- /tests/pools/common/test_exchange_reverts.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob") 6 | 7 | 8 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 9 | def test_insufficient_balance(bob, swap, coins, sending, receiving, decimals): 10 | amount = 10 ** decimals[sending] 11 | if coins[sending] == ETH_ADDRESS: 12 | value = amount 13 | else: 14 | coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 15 | value = 0 16 | 17 | with brownie.reverts(): 18 | swap.exchange(sending, receiving, amount + 1, 0, {"from": bob, "value": value}) 19 | 20 | 21 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 22 | def test_min_dy_too_high(bob, swap, coins, sending, receiving, decimals): 23 | amount = 10 ** decimals[sending] 24 | if coins[sending] == ETH_ADDRESS: 25 | value = amount 26 | else: 27 | coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 28 | value = 0 29 | 30 | min_dy = swap.get_dy(sending, receiving, amount) 31 | with brownie.reverts(): 32 | swap.exchange(sending, receiving, amount, min_dy + 2, {"from": bob, "value": value}) 33 | 34 | 35 | @pytest.mark.parametrize("idx", range(2)) 36 | def test_same_coin(bob, swap, idx): 37 | with brownie.reverts(): 38 | swap.exchange(idx, idx, 0, 0, {"from": bob}) 39 | 40 | 41 | @pytest.mark.parametrize("idx", [-1, -(2 ** 127)]) 42 | def test_i_below_zero(bob, swap, idx): 43 | with brownie.reverts(): 44 | swap.exchange(idx, 0, 0, 0, {"from": bob}) 45 | 46 | 47 | @pytest.mark.parametrize("idx", [9, 2 ** 127 - 1]) 48 | def test_i_above_n_coins(bob, swap, idx): 49 | with brownie.reverts(): 50 | swap.exchange(idx, 0, 0, 0, {"from": bob}) 51 | 52 | 53 | @pytest.mark.parametrize("idx", [-1, -(2 ** 127)]) 54 | def test_j_below_zero(bob, swap, idx): 55 | with brownie.reverts(): 56 | swap.exchange(0, idx, 0, 0, {"from": bob}) 57 | 58 | 59 | @pytest.mark.parametrize("idx", [9, 2 ** 127 - 1]) 60 | def test_j_above_n_coins(bob, swap, idx): 61 | with brownie.reverts(): 62 | swap.exchange(0, idx, 0, 0, {"from": bob}) 63 | 64 | 65 | def test_nonpayable(swap, bob): 66 | with brownie.reverts(): 67 | swap.exchange(0, 1, 0, 0, {"from": bob, "value": "1 ether"}) 68 | -------------------------------------------------------------------------------- /tests/pools/common/test_get_virtual_price.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob", "mint_bob") 4 | 5 | 6 | def test_number_go_up(bob, swap, initial_amounts, plain_pool_size, eth_amount): 7 | virtual_price = swap.get_virtual_price() 8 | 9 | for i, amount in enumerate(initial_amounts): 10 | amounts = [0] * plain_pool_size 11 | amounts[i] = amount 12 | swap.add_liquidity(amounts, 0, {"from": bob, "value": eth_amount(amounts[0])}) 13 | 14 | new_virtual_price = swap.get_virtual_price() 15 | assert new_virtual_price > virtual_price 16 | virtual_price = new_virtual_price 17 | 18 | 19 | @pytest.mark.parametrize("idx", range(2)) 20 | def test_remove_one_coin(alice, swap, idx): 21 | amount = swap.balanceOf(alice) // 10 22 | 23 | virtual_price = swap.get_virtual_price() 24 | swap.remove_liquidity_one_coin(amount, idx, 0, {"from": alice}) 25 | 26 | assert swap.get_virtual_price() > virtual_price 27 | 28 | 29 | @pytest.mark.parametrize("idx", range(2)) 30 | def test_remove_imbalance(alice, swap, idx, initial_amounts, plain_pool_size): 31 | amounts = [i // 2 for i in initial_amounts] 32 | amounts[idx] = 0 33 | 34 | virtual_price = swap.get_virtual_price() 35 | swap.remove_liquidity_imbalance( 36 | amounts, plain_pool_size * 1_000_000 * 10 ** 18, {"from": alice} 37 | ) 38 | 39 | assert swap.get_virtual_price() > virtual_price 40 | 41 | 42 | def test_remove(alice, swap, plain_pool_size, initial_amounts): 43 | withdraw_amount = sum(initial_amounts) // 2 44 | 45 | virtual_price = swap.get_virtual_price() 46 | swap.remove_liquidity(withdraw_amount, [0] * plain_pool_size, {"from": alice}) 47 | 48 | assert swap.get_virtual_price() >= virtual_price 49 | 50 | 51 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 52 | def test_exchange(bob, swap, sending, receiving, decimals, eth_amount): 53 | virtual_price = swap.get_virtual_price() 54 | 55 | amount = 10 ** decimals[sending] 56 | swap.exchange( 57 | sending, 58 | receiving, 59 | amount, 60 | 0, 61 | {"from": bob, "value": eth_amount(amount) if sending == 0 else 0}, 62 | ) 63 | 64 | assert swap.get_virtual_price() > virtual_price 65 | -------------------------------------------------------------------------------- /tests/pools/common/test_ramp_A_precise.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | MIN_RAMP_TIME = 86400 4 | 5 | 6 | def test_ramp_A(chain, alice, swap): 7 | initial_A = swap.initial_A() // 100 8 | future_time = chain.time() + MIN_RAMP_TIME + 5 9 | 10 | tx = swap.ramp_A(initial_A * 2, future_time, {"from": alice}) 11 | 12 | assert swap.initial_A() // 100 == initial_A 13 | assert swap.future_A() // 100 == initial_A * 2 14 | assert swap.initial_A_time() == tx.timestamp 15 | assert swap.future_A_time() == future_time 16 | 17 | 18 | def test_ramp_A_final(chain, alice, swap): 19 | initial_A = swap.initial_A() // 100 20 | future_time = chain.time() + 1000000 21 | 22 | swap.ramp_A(initial_A * 2, future_time, {"from": alice}) 23 | 24 | chain.sleep(1000000) 25 | chain.mine() 26 | 27 | assert swap.A() == initial_A * 2 28 | 29 | 30 | def test_ramp_A_value_up(chain, alice, swap): 31 | initial_A = swap.initial_A() // 100 32 | future_time = chain.time() + 1000000 33 | tx = swap.ramp_A(initial_A * 2, future_time, {"from": alice}) 34 | 35 | initial_time = tx.timestamp 36 | duration = future_time - tx.timestamp 37 | 38 | while chain.time() < future_time: 39 | chain.sleep(100000) 40 | chain.mine() 41 | expected = int(initial_A + ((chain.time() - initial_time) / duration) * initial_A) 42 | assert 0.999 < expected / swap.A() <= 1 43 | 44 | 45 | def test_ramp_A_value_down(chain, alice, swap): 46 | initial_A = swap.initial_A() // 100 47 | future_time = chain.time() + 1000000 48 | tx = swap.ramp_A(initial_A // 10, future_time, {"from": alice}) 49 | 50 | initial_time = tx.timestamp 51 | duration = future_time - tx.timestamp 52 | 53 | while chain.time() < future_time: 54 | chain.sleep(100000) 55 | chain.mine() 56 | expected = int( 57 | initial_A - ((chain.time() - initial_time) / duration) * (initial_A // 10 * 9) 58 | ) 59 | if expected == 0: 60 | assert swap.A() == initial_A // 10 61 | else: 62 | assert abs(swap.A() - expected) <= 1 63 | 64 | 65 | def test_stop_ramp_A(chain, alice, swap): 66 | initial_A = swap.initial_A() // 100 67 | future_time = chain.time() + 1000000 68 | swap.ramp_A(initial_A * 2, future_time, {"from": alice}) 69 | 70 | chain.sleep(31337) 71 | 72 | tx = swap.A.transact({"from": alice}) 73 | current_A = tx.return_value 74 | 75 | tx = swap.stop_ramp_A({"from": alice}) 76 | 77 | assert swap.initial_A() // 100 == current_A 78 | assert swap.future_A() // 100 == current_A 79 | assert swap.initial_A_time() == tx.timestamp 80 | assert swap.future_A_time() == tx.timestamp 81 | 82 | 83 | def test_ramp_A_only_owner(chain, bob, swap): 84 | with brownie.reverts(): 85 | swap.ramp_A(0, chain.time() + 1000000, {"from": bob}) 86 | 87 | 88 | def test_ramp_A_insufficient_time(chain, alice, swap): 89 | with brownie.reverts(): 90 | swap.ramp_A(0, chain.time() + MIN_RAMP_TIME - 1, {"from": alice}) 91 | 92 | 93 | def test_stop_ramp_A_only_owner(bob, swap): 94 | with brownie.reverts(): 95 | swap.stop_ramp_A({"from": bob}) 96 | -------------------------------------------------------------------------------- /tests/pools/common/test_receiver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob", "approve_bob") 5 | 6 | 7 | def test_add_liquidity(bob, charlie, swap, initial_amounts, eth_amount): 8 | swap.add_liquidity( 9 | initial_amounts, 0, charlie, {"from": bob, "value": eth_amount(initial_amounts[0])} 10 | ) 11 | 12 | assert swap.balanceOf(bob) == 0 13 | assert swap.balanceOf(charlie) > 0 14 | 15 | 16 | def test_exchange(alice, charlie, swap, coins): 17 | coins[1]._mint_for_testing(alice, 10 ** 18, {"from": alice}) 18 | 19 | swap.exchange(1, 0, 10 ** 18, 0, charlie, {"from": alice}) 20 | if coins[0] == ETH_ADDRESS: 21 | assert charlie.balance() > 0 22 | assert alice.balance() == 0 23 | else: 24 | assert coins[0].balanceOf(charlie) > 0 25 | assert coins[0].balanceOf(alice) == 0 26 | 27 | 28 | def test_remove_liquidity(alice, swap, charlie, coins, initial_amounts, plain_pool_size): 29 | initial_amount = swap.balanceOf(alice) 30 | withdraw_amount = initial_amount // 4 31 | charlie_pre_bal = charlie.balance() 32 | swap.remove_liquidity(withdraw_amount, [0] * plain_pool_size, charlie, {"from": alice}) 33 | 34 | for coin, amount in zip(coins, initial_amounts): 35 | if coin == ETH_ADDRESS: 36 | assert swap.balance() + charlie.balance() - charlie_pre_bal == amount 37 | continue 38 | 39 | assert coin.balanceOf(swap) + coin.balanceOf(charlie) == amount 40 | 41 | assert swap.balanceOf(alice) == initial_amount - withdraw_amount 42 | assert swap.totalSupply() == initial_amount - withdraw_amount 43 | 44 | 45 | def test_remove_imbalanced(alice, charlie, swap, coins, initial_amounts): 46 | initial_balance = swap.balanceOf(alice) 47 | amounts = [i // 4 for i in initial_amounts] 48 | charlie_pre_bal = charlie.balance() 49 | swap.remove_liquidity_imbalance(amounts, initial_balance, charlie, {"from": alice}) 50 | 51 | for i, coin in enumerate(coins): 52 | if coin == ETH_ADDRESS: 53 | assert charlie.balance() - charlie_pre_bal == amounts[i] 54 | assert swap.balance() == initial_amounts[i] - amounts[i] 55 | continue 56 | 57 | assert coin.balanceOf(charlie) == amounts[i] 58 | assert coin.balanceOf(swap) == initial_amounts[i] - amounts[i] 59 | 60 | assert swap.balanceOf(alice) / initial_balance == 0.75 61 | 62 | 63 | def test_remove_one_coin(alice, charlie, swap, coins, is_eth_pool): 64 | wrapped = coins[0] 65 | swap.remove_liquidity_one_coin(10 ** 18, 0, 0, charlie, {"from": alice}) 66 | 67 | if is_eth_pool: 68 | assert charlie.balance() > 0 69 | assert alice.balance() == 0 70 | else: 71 | assert wrapped.balanceOf(charlie) > 0 72 | assert wrapped.balanceOf(alice) == 0 73 | 74 | assert swap.balanceOf(charlie) == 0 75 | assert swap.balanceOf(alice) > 0 76 | -------------------------------------------------------------------------------- /tests/pools/common/test_remove_liquidity.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | @pytest.mark.parametrize("min_amount", (0, 1)) 9 | def test_remove_liquidity(alice, swap, coins, min_amount, initial_amounts): 10 | swap.remove_liquidity( 11 | swap.balanceOf(alice), [i * min_amount for i in initial_amounts], {"from": alice} 12 | ) 13 | 14 | for coin, amount in zip(coins, initial_amounts): 15 | if coin == ETH_ADDRESS: 16 | assert alice.balance() == amount 17 | assert swap.balance() == 0 18 | continue 19 | 20 | assert coin.balanceOf(alice) == amount 21 | assert coin.balanceOf(swap) == 0 22 | 23 | assert swap.balanceOf(alice) == 0 24 | assert swap.totalSupply() == 0 25 | 26 | 27 | def test_remove_partial(alice, swap, coins, initial_amounts, plain_pool_size): 28 | initial_amount = swap.balanceOf(alice) 29 | withdraw_amount = initial_amount // 2 30 | swap.remove_liquidity(withdraw_amount, [0] * plain_pool_size, {"from": alice}) 31 | 32 | for coin, amount in zip(coins, initial_amounts): 33 | if coin == ETH_ADDRESS: 34 | assert swap.balance() + alice.balance() == amount 35 | continue 36 | 37 | assert coin.balanceOf(swap) + coin.balanceOf(alice) == amount 38 | 39 | assert swap.balanceOf(alice) == initial_amount - withdraw_amount 40 | assert swap.totalSupply() == initial_amount - withdraw_amount 41 | 42 | 43 | @pytest.mark.parametrize("idx", range(2)) 44 | def test_below_min_amount(alice, swap, initial_amounts, idx): 45 | min_amount = initial_amounts.copy() 46 | min_amount[idx] += 1 47 | 48 | with brownie.reverts(): 49 | swap.remove_liquidity(swap.balanceOf(alice), min_amount, {"from": alice}) 50 | 51 | 52 | def test_amount_exceeds_balance(alice, swap, plain_pool_size): 53 | with brownie.reverts(): 54 | swap.remove_liquidity(swap.balanceOf(alice) + 1, [0] * plain_pool_size, {"from": alice}) 55 | 56 | 57 | def test_event(alice, bob, swap, coins, plain_pool_size): 58 | swap.transfer(bob, 10 ** 18, {"from": alice}) 59 | tx = swap.remove_liquidity(10 ** 18, [0] * plain_pool_size, {"from": bob}) 60 | 61 | event = tx.events["RemoveLiquidity"] 62 | assert event["provider"] == bob 63 | assert event["token_supply"] == swap.totalSupply() 64 | for coin, amount in zip(coins, event["token_amounts"]): 65 | if coin == ETH_ADDRESS: 66 | assert bob.balance() - 1_000_000 * 10 ** 18 == amount 67 | continue 68 | assert coin.balanceOf(bob) == amount 69 | -------------------------------------------------------------------------------- /tests/pools/common/test_remove_liquidity_imbalance.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | @pytest.mark.parametrize("divisor", [2, 5, 10]) 9 | def test_remove_balanced(alice, swap, coins, divisor, initial_amounts): 10 | initial_balance = swap.balanceOf(alice) 11 | amounts = [i // divisor for i in initial_amounts] 12 | swap.remove_liquidity_imbalance(amounts, initial_balance, {"from": alice}) 13 | 14 | for i, coin in enumerate(coins): 15 | if coin == ETH_ADDRESS: 16 | assert alice.balance() == amounts[i] 17 | assert swap.balance() == initial_amounts[i] - amounts[i] 18 | continue 19 | 20 | assert coin.balanceOf(alice) == amounts[i] 21 | assert coin.balanceOf(swap) == initial_amounts[i] - amounts[i] 22 | 23 | assert swap.balanceOf(alice) / initial_balance == 1 - 1 / divisor 24 | 25 | 26 | @pytest.mark.parametrize("idx", range(2)) 27 | def test_remove_one(alice, swap, coins, plain_pool_size, idx, initial_amounts): 28 | amounts = [0] * plain_pool_size 29 | amounts[idx] = initial_amounts[idx] // 2 30 | 31 | lp_balance = plain_pool_size * 1_000_000 * 10 ** 18 32 | swap.remove_liquidity_imbalance(amounts, lp_balance, {"from": alice}) 33 | 34 | for i, coin in enumerate(coins): 35 | if coin == ETH_ADDRESS: 36 | assert alice.balance() == amounts[i] 37 | assert swap.balance() == initial_amounts[i] - amounts[i] 38 | continue 39 | 40 | assert coin.balanceOf(alice) == amounts[i] 41 | assert coin.balanceOf(swap) == initial_amounts[i] - amounts[i] 42 | 43 | actual_balance = swap.balanceOf(alice) 44 | actual_total_supply = swap.totalSupply() 45 | ideal_balance = (2 * plain_pool_size - 1) * lp_balance / (2 * plain_pool_size) 46 | 47 | assert actual_balance == actual_total_supply 48 | assert ideal_balance * 0.9994 < actual_balance < ideal_balance 49 | 50 | 51 | @pytest.mark.parametrize("divisor", [1, 2, 10]) 52 | def test_exceed_max_burn(alice, swap, plain_pool_size, divisor, initial_amounts): 53 | amounts = [i // divisor for i in initial_amounts] 54 | max_burn = plain_pool_size * 1_000_000 * 10 ** 18 // divisor 55 | 56 | with brownie.reverts(): 57 | swap.remove_liquidity_imbalance(amounts, max_burn - 1, {"from": alice}) 58 | 59 | 60 | def test_cannot_remove_zero(alice, swap, plain_pool_size): 61 | with brownie.reverts(): 62 | swap.remove_liquidity_imbalance([0] * plain_pool_size, 0, {"from": alice}) 63 | 64 | 65 | def test_no_totalsupply(alice, swap, plain_pool_size): 66 | swap.remove_liquidity(swap.totalSupply(), [0] * plain_pool_size, {"from": alice}) 67 | with brownie.reverts(): 68 | swap.remove_liquidity_imbalance([0] * plain_pool_size, 0, {"from": alice}) 69 | 70 | 71 | def test_event(alice, bob, swap, coins, plain_pool_size, initial_amounts): 72 | swap.transfer(bob, swap.balanceOf(alice), {"from": alice}) 73 | amounts = [i // 5 for i in initial_amounts] 74 | max_burn = plain_pool_size * 1_000_000 * 10 ** 18 75 | 76 | tx = swap.remove_liquidity_imbalance(amounts, max_burn, {"from": bob}) 77 | 78 | event = tx.events["RemoveLiquidityImbalance"] 79 | assert event["provider"] == bob 80 | assert event["token_supply"] == swap.totalSupply() 81 | for coin, amount in zip(coins, event["token_amounts"]): 82 | if coin == ETH_ADDRESS: 83 | bob.balance() == amount + 1_000_000 * 10 ** 18 84 | continue 85 | assert coin.balanceOf(bob) == amount 86 | -------------------------------------------------------------------------------- /tests/pools/common/test_remove_liquidity_one_coin.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ETH_ADDRESS 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | @pytest.mark.parametrize("idx", range(2)) 9 | def test_amount_received(alice, swap, coins, decimals, idx): 10 | decimals = decimals[idx] 11 | wrapped = coins[idx] 12 | 13 | swap.remove_liquidity_one_coin(10 ** 18, idx, 0, {"from": alice}) 14 | 15 | ideal = 10 ** decimals 16 | 17 | if wrapped == ETH_ADDRESS: 18 | assert ideal * 0.99 <= alice.balance() <= ideal 19 | else: 20 | assert ideal * 0.99 <= wrapped.balanceOf(alice) <= ideal 21 | 22 | 23 | @pytest.mark.parametrize("idx", range(2)) 24 | @pytest.mark.parametrize("divisor", [1, 5, 42]) 25 | def test_lp_token_balance(alice, swap, idx, divisor): 26 | initial_amount = swap.balanceOf(alice) 27 | amount = initial_amount // divisor 28 | 29 | swap.remove_liquidity_one_coin(amount, idx, 0, {"from": alice}) 30 | 31 | assert swap.balanceOf(alice) + amount == initial_amount 32 | 33 | 34 | @pytest.mark.parametrize("idx", range(2)) 35 | def test_expected_vs_actual(alice, swap, coins, idx): 36 | amount = swap.balanceOf(alice) // 10 37 | 38 | expected = swap.calc_withdraw_one_coin(amount, idx) 39 | swap.remove_liquidity_one_coin(amount, idx, 0, {"from": alice}) 40 | 41 | if coins[idx] == ETH_ADDRESS: 42 | assert alice.balance() == expected 43 | else: 44 | assert coins[idx].balanceOf(alice) == expected 45 | 46 | 47 | @pytest.mark.parametrize("idx", range(2)) 48 | def test_below_min_amount(alice, swap, idx): 49 | amount = swap.balanceOf(alice) 50 | 51 | expected = swap.calc_withdraw_one_coin(amount, idx) 52 | with brownie.reverts(): 53 | swap.remove_liquidity_one_coin(amount, idx, expected + 1, {"from": alice}) 54 | 55 | 56 | @pytest.mark.parametrize("idx", range(2)) 57 | def test_amount_exceeds_balance(bob, swap, idx): 58 | with brownie.reverts(): 59 | swap.remove_liquidity_one_coin(1, idx, 0, {"from": bob}) 60 | 61 | 62 | def test_below_zero(alice, swap): 63 | with brownie.reverts(): 64 | swap.remove_liquidity_one_coin(1, -1, 0, {"from": alice}) 65 | 66 | 67 | def test_above_n_coins(alice, swap, plain_pool_size): 68 | with brownie.reverts(): 69 | swap.remove_liquidity_one_coin(1, plain_pool_size, 0, {"from": alice}) 70 | 71 | 72 | @pytest.mark.parametrize("idx", range(2)) 73 | def test_event(alice, bob, swap, idx, coins): 74 | swap.transfer(bob, 10 ** 18, {"from": alice}) 75 | 76 | tx = swap.remove_liquidity_one_coin(10 ** 18, idx, 0, {"from": bob}) 77 | 78 | event = tx.events["RemoveLiquidityOne"] 79 | assert event["provider"] == bob 80 | assert event["token_amount"] == 10 ** 18 81 | 82 | coin = coins[idx] 83 | if coin == ETH_ADDRESS: 84 | assert bob.balance() - 1_000_000 * 10 ** 18 == event["coin_amount"] 85 | else: 86 | assert coin.balanceOf(bob) == event["coin_amount"] 87 | -------------------------------------------------------------------------------- /tests/pools/common/test_withdraw_admin_fees.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS, accounts 3 | 4 | SWAP_AMOUNT = 1e6 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(alice, factory, bob, swap, coins, add_initial_liquidity, eth_amount, decimals): 9 | amounts = [0] * len(coins) 10 | for idx, coin in enumerate(coins[:2]): 11 | amount = 1e6 * 10 ** decimals[idx] 12 | if coin == ETH_ADDRESS: 13 | accounts[-1].transfer(alice, amount) 14 | continue 15 | amounts[idx] = amount 16 | coin._mint_for_testing(alice, amount, {"from": alice}) 17 | coin.approve(swap, 2 ** 256 - 1, {"from": alice}) 18 | 19 | swap.add_liquidity(amounts, 0, {"from": alice, "value": eth_amount(amounts[0])}) 20 | 21 | # factory.set_fee_receiver(ZERO_ADDRESS, charlie) 22 | 23 | amount = SWAP_AMOUNT * 10 ** 18 24 | 25 | if coins[0] == ETH_ADDRESS: 26 | accounts[-1].transfer(bob, amount) 27 | else: 28 | coins[0]._mint_for_testing(bob, amount, {"from": bob}) 29 | coins[0].approve(swap, amount, {"from": bob}) 30 | swap.exchange(0, 1, amount, 0, {"from": bob, "value": eth_amount(amount)}) 31 | 32 | 33 | def test_withdraw_admin_fees(bob, coins, swap, fee_receiver): 34 | fees = [] 35 | pre_balance = fee_receiver.balance() 36 | for i, coin in enumerate(coins[:2]): 37 | if coin == ETH_ADDRESS: 38 | pass 39 | else: 40 | assert coin.balanceOf(fee_receiver) == 0 41 | fees.append(swap.admin_balances(i)) 42 | 43 | swap.withdraw_admin_fees({"from": bob}) 44 | for i, coin in enumerate(coins[:2]): 45 | if coin == ETH_ADDRESS: 46 | assert fee_receiver.balance() - pre_balance == fees[i] 47 | continue 48 | 49 | assert coin.balanceOf(fee_receiver) == fees[i] 50 | -------------------------------------------------------------------------------- /tests/pools/meta/test_exchange_meta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pytest import approx 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob") 5 | 6 | 7 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 8 | def test_exchange( 9 | bob, 10 | swap, 11 | coins, 12 | sending, 13 | receiving, 14 | decimals, 15 | base_pool, 16 | ): 17 | 18 | amount = 10 ** decimals[sending] 19 | if sending == 0: 20 | amount = amount * base_pool.get_virtual_price() // 10 ** 18 21 | else: 22 | amount = amount * 10 ** 18 // base_pool.get_virtual_price() 23 | coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 24 | 25 | swap.exchange(sending, receiving, amount, 0, {"from": bob}) 26 | 27 | assert coins[sending].balanceOf(bob) == 0 28 | 29 | received = coins[receiving].balanceOf(bob) 30 | assert 1 - max(1e-4, 1 / received) - 0.0004 < received / 10 ** decimals[receiving] < 1 - 0.0004 31 | 32 | expected_admin_fee = 10 ** decimals[receiving] * 0.0004 * 0.5 33 | admin_fee = swap.admin_balances(receiving) 34 | 35 | assert expected_admin_fee / admin_fee == approx( 36 | 1, rel=max(1e-3, 1 / (expected_admin_fee - 1.1)) 37 | ) 38 | -------------------------------------------------------------------------------- /tests/pools/meta/test_exchange_underlying.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import pytest 4 | from pytest import approx 5 | 6 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob_underlying") 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "sending,receiving", filter(lambda k: 0 in k, itertools.permutations(range(4), 2)) 11 | ) 12 | def test_amounts( 13 | bob, 14 | swap, 15 | underlying_coins, 16 | sending, 17 | receiving, 18 | underlying_decimals, 19 | ): 20 | amount = 10 ** underlying_decimals[sending] 21 | underlying_coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 22 | swap.exchange_underlying(sending, receiving, amount, 0, {"from": bob}) 23 | 24 | assert underlying_coins[sending].balanceOf(bob) == 0 25 | 26 | received = underlying_coins[receiving].balanceOf(bob) 27 | assert 0.999 <= received / 10 ** underlying_decimals[receiving] < 1 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "sending,receiving", filter(lambda k: 0 in k, itertools.permutations(range(4), 2)) 32 | ) 33 | def test_fees( 34 | bob, 35 | swap, 36 | underlying_coins, 37 | sending, 38 | receiving, 39 | underlying_decimals, 40 | decimals, 41 | base_pool, 42 | ): 43 | amount = 10000 * 10 ** underlying_decimals[sending] 44 | underlying_coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 45 | swap.exchange_underlying(sending, receiving, amount, 0, {"from": bob}) 46 | 47 | admin_idx = min(1, receiving) 48 | admin_fee = swap.admin_balances(admin_idx) 49 | 50 | expected = 2 * 10 ** decimals[admin_idx] 51 | if admin_idx == 1: 52 | expected = expected * 10 ** 18 // base_pool.get_virtual_price() 53 | assert expected / admin_fee == approx(1, rel=1e-3) 54 | 55 | 56 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(4), 2)) 57 | def test_min_dy_underlying(bob, swap, underlying_coins, sending, receiving, underlying_decimals): 58 | amount = 10 ** underlying_decimals[sending] 59 | underlying_coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 60 | 61 | expected = swap.get_dy_underlying(sending, receiving, amount) 62 | tx = swap.exchange_underlying(sending, receiving, amount, 0, {"from": bob}) 63 | received = tx.events["TokenExchangeUnderlying"]["tokens_bought"] 64 | 65 | assert abs(expected - received) / received < 0.00001 66 | -------------------------------------------------------------------------------- /tests/pools/meta/test_exchange_underlying_reverts.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import brownie 4 | import pytest 5 | from brownie import ETH_ADDRESS 6 | 7 | pytestmark = pytest.mark.usefixtures( 8 | "add_initial_liquidity", "approve_bob_underlying", "mint_bob_underlying" 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(4), 2)) 13 | def test_min_dy_too_high(bob, swap, underlying_coins, underlying_decimals, sending, receiving): 14 | amount = 10 ** underlying_decimals[sending] 15 | 16 | underlying_coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 17 | 18 | min_dy = swap.get_dy_underlying(sending, receiving, amount * 1.0001) 19 | with brownie.reverts(): 20 | swap.exchange_underlying(sending, receiving, amount, min_dy, {"from": bob}) 21 | 22 | 23 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(4), 2)) 24 | def test_insufficient_balance( 25 | bob, swap, underlying_coins, underlying_decimals, sending, receiving 26 | ): # noqa 27 | amount = 10 ** underlying_decimals[sending] 28 | 29 | if (balance := underlying_coins[sending].balanceOf(bob)) > 0: 30 | underlying_coins[sending].transfer(ETH_ADDRESS, balance, {"from": bob}) 31 | 32 | underlying_coins[sending]._mint_for_testing(bob, amount, {"from": bob}) 33 | with brownie.reverts(): 34 | swap.exchange_underlying(sending, receiving, amount + 1, 0, {"from": bob}) 35 | 36 | 37 | @pytest.mark.parametrize("idx", range(4)) 38 | def test_same_coin(bob, swap, idx): 39 | with brownie.reverts(): 40 | swap.exchange_underlying(idx, idx, 0, 0, {"from": bob}) 41 | 42 | 43 | @pytest.mark.parametrize("idx", [-1, -(2 ** 127)]) 44 | def test_i_below_zero(bob, swap, idx): 45 | with brownie.reverts(): 46 | swap.exchange_underlying(idx, 0, 0, 0, {"from": bob}) 47 | 48 | 49 | @pytest.mark.parametrize("idx", [4, 2 ** 127 - 1]) 50 | def test_i_above_n_coins(bob, swap, idx): 51 | with brownie.reverts(): 52 | swap.exchange_underlying(idx, 0, 0, 0, {"from": bob}) 53 | 54 | 55 | @pytest.mark.parametrize("idx", [-1, -(2 ** 127)]) 56 | def test_j_below_zero(bob, swap, idx): 57 | with brownie.reverts(): 58 | swap.exchange_underlying(0, idx, 0, 0, {"from": bob}) 59 | 60 | 61 | @pytest.mark.parametrize("idx", [4, 2 ** 127 - 1]) 62 | def test_j_above_n_coins(bob, swap, idx): 63 | with brownie.reverts(): 64 | swap.exchange_underlying(0, idx, 0, 0, {"from": bob}) 65 | -------------------------------------------------------------------------------- /tests/pools/meta/test_get_virtual_price_meta.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import pytest 4 | 5 | pytestmark = pytest.mark.usefixtures( 6 | "add_initial_liquidity", "approve_bob_underlying", "mint_bob_underlying" 7 | ) 8 | 9 | 10 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(4), 2)) 11 | def test_exchange_underlying(bob, swap, sending, receiving, underlying_decimals): # noqa 12 | virtual_price = swap.get_virtual_price() 13 | 14 | amount = 10 ** underlying_decimals[sending] 15 | swap.exchange_underlying(sending, receiving, amount, 0, {"from": bob}) 16 | 17 | assert swap.get_virtual_price() > virtual_price 18 | -------------------------------------------------------------------------------- /tests/pools/meta/test_receiver_meta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_alice_underlying") 4 | 5 | 6 | def test_exchange_underlying(alice, charlie, swap, underlying_coins, underlying_decimals): 7 | amount = 10 ** underlying_decimals[1] 8 | underlying_coins[1]._mint_for_testing(alice, amount) 9 | 10 | swap.exchange_underlying(1, 0, amount, 0, charlie, {"from": alice}) 11 | assert underlying_coins[0].balanceOf(charlie) > 0 12 | assert underlying_coins[0].balanceOf(alice) == 0 13 | -------------------------------------------------------------------------------- /tests/pools/rebase/test_token_balances.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(autouse=True) 5 | def setup(add_initial_liquidity): 6 | pass 7 | 8 | 9 | def test_get_balances(initial_amounts, swap, plain_pool_size): 10 | balances = swap.get_balances() 11 | assert len(balances) == plain_pool_size 12 | assert balances[0] == initial_amounts[0] 13 | assert balances[1] == initial_amounts[1] 14 | 15 | 16 | def test_admin_balances_do_not_change(alice, swap, coins, initial_amounts): 17 | n_coins = len(coins) 18 | for i in range(n_coins): 19 | assert swap.admin_balances(i) == 0 20 | 21 | for i, coin in enumerate(coins): 22 | coin._mint_for_testing(swap, initial_amounts[i], {"from": alice}) 23 | 24 | for i in range(n_coins): 25 | assert swap.admin_balances(i) == 0 26 | 27 | 28 | def test_balances(swap, coins, initial_amounts): 29 | for i in range(len(coins)): 30 | assert swap.balances(i) == initial_amounts[i] 31 | 32 | 33 | def test_virtual_price_increases_with_balances(alice, swap, coins, initial_amounts): 34 | virtual_price = swap.get_virtual_price() 35 | 36 | for i, coin in enumerate(coins): 37 | coin._mint_for_testing(swap, initial_amounts[i], {"from": alice}) 38 | 39 | assert swap.get_virtual_price() // 2 == virtual_price 40 | 41 | 42 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 43 | def test_admin_balances( 44 | alice, 45 | swap, 46 | coins, 47 | initial_amounts, 48 | sending, 49 | receiving, 50 | ): 51 | for coin in coins: 52 | coin.approve(swap, 2 ** 256 - 1, {"from": alice}) 53 | 54 | for coin, amount in zip(coins, initial_amounts): 55 | coin._mint_for_testing(alice, amount, {"from": alice}) 56 | 57 | for send, recv in [(sending, receiving), (receiving, sending)]: 58 | swap.exchange(send, recv, initial_amounts[send], 0, {"from": alice}) 59 | 60 | for i in (sending, receiving): 61 | admin_fee = swap.admin_balances(i) 62 | assert coins[i].balanceOf(swap) - admin_fee == swap.balances(i) 63 | assert admin_fee > 0 64 | 65 | 66 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 67 | def test_withdraw_one_coin( 68 | alice, 69 | swap, 70 | coins, 71 | sending, 72 | receiving, 73 | initial_amounts, 74 | ): 75 | 76 | for coin in coins: 77 | coin.approve(swap, 2 ** 256 - 1, {"from": alice}) 78 | 79 | for coin, amount in zip(coins, initial_amounts): 80 | coin._mint_for_testing(alice, amount, {"from": alice}) 81 | 82 | swap.exchange(sending, receiving, initial_amounts[sending], 0, {"from": alice}) 83 | 84 | admin_balance = swap.admin_balances(receiving) 85 | 86 | assert admin_balance > 0 87 | assert swap.admin_balances(sending) == 0 88 | 89 | swap.withdraw_admin_fees({"from": alice}) 90 | 91 | assert swap.balances(receiving) == coins[receiving].balanceOf(swap) 92 | 93 | 94 | def test_no_fees(bob, fee_receiver, coins, swap): 95 | swap.withdraw_admin_fees({"from": bob}) 96 | 97 | for coin in coins: 98 | assert coin.balanceOf(fee_receiver) == 0 99 | -------------------------------------------------------------------------------- /tests/test_factory.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import brownie 4 | import pytest 5 | from brownie import ZERO_ADDRESS, Contract 6 | from brownie_tokens import ERC20 7 | 8 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_bob", "mint_bob") 9 | 10 | 11 | @pytest.fixture 12 | def new_factory(alice, Factory, frank): 13 | return Factory.deploy(frank, {"from": alice}) 14 | 15 | 16 | @pytest.fixture 17 | def new_factory_setup( 18 | new_factory, 19 | plain_basic, 20 | plain_pool_size, 21 | alice, 22 | fee_receiver, 23 | ): 24 | new_factory.set_plain_implementations( 25 | plain_pool_size, [plain_basic] + [ZERO_ADDRESS] * 9, {"from": alice} 26 | ) 27 | 28 | 29 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 30 | def test_find_pool_for_coins(factory, swap, coins, sending, receiving): 31 | assert factory.find_pool_for_coins(coins[sending], coins[receiving]) == swap 32 | 33 | 34 | @pytest.mark.skip 35 | @pytest.mark.parametrize("idx", range(1, 4)) 36 | def test_find_pool_for_coins_underlying(factory, is_rebase_pool, swap, underlying_coins, idx): 37 | if not is_rebase_pool: 38 | pytest.skip() 39 | assert factory.find_pool_for_coins(underlying_coins[0], underlying_coins[idx]) == swap 40 | assert factory.find_pool_for_coins(underlying_coins[idx], underlying_coins[0]) == swap 41 | 42 | 43 | @pytest.mark.skip 44 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(1, 4), 2)) 45 | def test_find_pool_underlying_base_pool_only(factory, underlying_coins, sending, receiving): 46 | assert ( 47 | factory.find_pool_for_coins(underlying_coins[sending], underlying_coins[receiving]) 48 | == ZERO_ADDRESS 49 | ) 50 | 51 | 52 | @pytest.mark.skip 53 | def test_factory(factory, swap): 54 | assert factory.get_meta_n_coins(swap) == [2, 4] 55 | 56 | 57 | def test_get_coins(factory, swap, coins, plain_pool_size): 58 | _coins = factory.get_coins(swap) 59 | assert _coins == coins + [ZERO_ADDRESS] * (4 - plain_pool_size) 60 | 61 | 62 | @pytest.mark.skip 63 | def test_get_underlying_coins(factory, swap, underlying_coins, plain_pool_size): 64 | assert factory.get_underlying_coins(swap) == underlying_coins + [ZERO_ADDRESS] * ( 65 | 4 - plain_pool_size 66 | ) 67 | 68 | 69 | def test_get_decimals(factory, swap, decimals): 70 | _decimals = factory.get_decimals(swap) 71 | assert _decimals == decimals + [0] * (4 - len(decimals)) 72 | 73 | 74 | @pytest.mark.skip 75 | def test_get_underlying_decimals(factory, swap, underlying_decimals): 76 | assert factory.get_underlying_decimals(swap) == underlying_decimals + [0] * ( 77 | 4 - len(underlying_decimals) 78 | ) 79 | 80 | 81 | @pytest.mark.skip 82 | def test_get_metapool_rates(factory, swap, base_pool): 83 | assert factory.get_metapool_rates(swap) == [10 ** 18, base_pool.get_virtual_price()] 84 | 85 | 86 | def test_get_balances(factory, swap, plain_pool_size): 87 | assert factory.get_balances(swap) == [swap.balances(i) for i in range(plain_pool_size)] + [ 88 | 0 89 | ] * (4 - plain_pool_size) 90 | 91 | 92 | @pytest.mark.skip 93 | def test_get_underlying_balances(factory, swap, is_rebase_pool): 94 | with brownie.reverts("dev: pool is not a metapool"): 95 | factory.get_underlying_balances(swap) 96 | return 97 | 98 | # if not is_rebase_pool: 99 | # pytest.skip() 100 | # balances = factory.get_underlying_balances(swap) 101 | # for i in range(4): 102 | # assert balances[i] == swap.balances(i) 103 | # assert balances[4:] == [0] * 4 104 | 105 | 106 | def test_get_A(factory, swap): 107 | assert factory.get_A(swap) == swap.A() 108 | 109 | 110 | def test_get_fees(factory, swap): 111 | assert factory.get_fees(swap) == [swap.fee(), swap.admin_fee()] 112 | 113 | 114 | def test_get_admin_balances(factory, swap, plain_pool_size): 115 | assert factory.get_admin_balances(swap) == [ 116 | swap.admin_balances(i) for i in range(plain_pool_size) 117 | ] + [0] * (4 - plain_pool_size) 118 | 119 | 120 | @pytest.mark.skip 121 | @pytest.mark.parametrize("sending,receiving", itertools.permutations(range(1, 4), 2)) 122 | def test_get_coin_indices_underlying(factory, swap, sending, receiving, underlying_coins): 123 | i, j, is_underlying = factory.get_coin_indices( 124 | swap, underlying_coins[sending], underlying_coins[receiving] 125 | ) 126 | assert i == sending 127 | assert j == receiving 128 | assert is_underlying is False 129 | 130 | 131 | @pytest.mark.parametrize("sending,receiving", [(0, 1), (1, 0)]) 132 | def test_get_coin_indices(factory, swap, sending, receiving, coins): 133 | i, j, is_underlying = factory.get_coin_indices(swap, coins[sending], coins[receiving]) 134 | assert i == sending 135 | assert j == receiving 136 | 137 | 138 | @pytest.mark.skip 139 | @pytest.mark.parametrize("idx", range(1, 4)) 140 | def test_get_coin_indices_reverts(factory, swap, base_lp_token, underlying_coins, idx): 141 | with brownie.reverts(): 142 | factory.get_coin_indices(swap, base_lp_token, underlying_coins[idx]) 143 | 144 | 145 | @pytest.mark.skip 146 | def test_add_base_pool(factory, alice, fee_receiver, implementation_usd): 147 | susd_pool = "0xA5407eAE9Ba41422680e2e00537571bcC53efBfD" 148 | factory.add_base_pool( 149 | susd_pool, fee_receiver, 0, [implementation_usd] + [ZERO_ADDRESS] * 9, {"from": alice} 150 | ) 151 | assert factory.base_pool_count() == 3 152 | assert factory.base_pool_list(2) == susd_pool 153 | assert factory.get_fee_receiver(susd_pool) == fee_receiver 154 | 155 | 156 | @pytest.mark.skip 157 | def test_add_base_pool_already_exists(factory, base_pool, alice, fee_receiver, implementation_usd): 158 | with brownie.reverts("dev: pool exists"): 159 | factory.add_base_pool( 160 | base_pool, fee_receiver, 0, [implementation_usd] + [ZERO_ADDRESS] * 9, {"from": alice} 161 | ) 162 | 163 | 164 | @pytest.mark.skip 165 | def test_add_base_pool_only_admin(factory, base_pool, bob, fee_receiver, implementation_usd): 166 | with brownie.reverts("dev: admin-only function"): 167 | factory.add_base_pool( 168 | "0xA5407eAE9Ba41422680e2e00537571bcC53efBfD", 169 | fee_receiver, 170 | 0, 171 | [implementation_usd] + [ZERO_ADDRESS] * 9, 172 | {"from": bob}, 173 | ) 174 | 175 | 176 | @pytest.mark.skip 177 | def test_deploy_metapool(MetaUSD, new_factory, new_factory_setup, base_pool, bob): 178 | coin = ERC20(decimals=7) 179 | 180 | tx = new_factory.deploy_metapool( 181 | base_pool, "Name", "SYM", coin, 12345, 50000000, 0, {"from": bob} 182 | ) 183 | assert tx.return_value == tx.new_contracts[0] 184 | swap = MetaUSD.at(tx.return_value) 185 | 186 | assert swap.coins(0) == coin 187 | assert swap.A() == 12345 188 | assert swap.fee() == 50000000 189 | 190 | assert new_factory.pool_count() == 1 191 | assert new_factory.pool_list(0) == swap 192 | assert new_factory.get_decimals(swap) == [7, 18, 0, 0] 193 | 194 | 195 | @pytest.mark.skip 196 | def test_add_existing_metapools( 197 | factory, new_factory, fee_receiver, implementation_usd, base_pool, alice 198 | ): 199 | assert new_factory.pool_count() == 0 200 | # add existing USD pools to new factory 201 | new_factory.add_base_pool( 202 | base_pool, fee_receiver, 0, [implementation_usd] + [ZERO_ADDRESS] * 9, {"from": alice} 203 | ) 204 | new_factory.add_existing_metapools( 205 | ["0x5a6A4D54456819380173272A5E8E9B9904BdF41B", "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c"] 206 | + [ZERO_ADDRESS] * 8 207 | ) 208 | assert new_factory.pool_count() == 2 209 | assert new_factory.pool_list(0) == "0x5a6A4D54456819380173272A5E8E9B9904BdF41B" 210 | assert new_factory.pool_list(1) == "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c" 211 | assert ( 212 | new_factory.get_implementation_address("0x5a6A4D54456819380173272A5E8E9B9904BdF41B") 213 | == "0x5F890841f657d90E081bAbdB532A05996Af79Fe6" 214 | ) 215 | 216 | 217 | @pytest.mark.skip 218 | def test_add_existing_metapools_unknown_pool(swap, new_factory): 219 | with brownie.reverts("dev: pool not in old factory"): 220 | new_factory.add_existing_metapools([swap] + [ZERO_ADDRESS] * 9) 221 | 222 | 223 | @pytest.mark.skip 224 | def test_add_existing_metapools_duplicate_pool( 225 | new_factory, base_pool, implementation_usd, fee_receiver, alice 226 | ): 227 | new_factory.add_base_pool( 228 | base_pool, fee_receiver, 0, [implementation_usd] + [ZERO_ADDRESS] * 9, {"from": alice} 229 | ) 230 | new_factory.add_existing_metapools( 231 | ["0x5a6A4D54456819380173272A5E8E9B9904BdF41B"] + [ZERO_ADDRESS] * 9 232 | ) 233 | with brownie.reverts("dev: pool already exists"): 234 | new_factory.add_existing_metapools( 235 | ["0x5a6A4D54456819380173272A5E8E9B9904BdF41B"] + [ZERO_ADDRESS] * 9 236 | ) 237 | 238 | 239 | def test_deploy_plain_pool( 240 | new_factory_setup, new_factory, decimals, bob, plain_basic, project, coins 241 | ): 242 | 243 | tx = new_factory.deploy_plain_pool( 244 | "Test Plain", 245 | "TST", 246 | coins + [ZERO_ADDRESS] * (4 - len(coins)), 247 | 12345, 248 | 50000000, 249 | 0, 250 | 0, 251 | {"from": bob}, 252 | ) 253 | assert tx.return_value == tx.new_contracts[0] 254 | swap = getattr(project, plain_basic._name).at(tx.return_value) 255 | 256 | assert swap.coins(0) == coins[0] 257 | assert swap.coins(1) == coins[1] 258 | 259 | assert swap.A() == 12345 260 | assert swap.fee() == 50000000 261 | 262 | assert new_factory.pool_count() == 1 263 | assert new_factory.pool_list(0) == swap 264 | assert new_factory.get_decimals(swap) == decimals + [0] * (4 - len(decimals)) 265 | 266 | 267 | @pytest.mark.skip 268 | def test_pool_count(new_factory, coins, new_factory_setup, bob, base_pool): 269 | 270 | tx = new_factory.deploy_plain_pool( 271 | "Test Plain", "TST", coins, 12345, 50000000, 0, 0, {"from": bob} 272 | ) 273 | assert tx.return_value == tx.new_contracts[0] 274 | assert new_factory.pool_count() == 1 275 | 276 | coin = ERC20(decimals=7) 277 | tx = new_factory.deploy_metapool( 278 | base_pool, "Name", "SYM", coin, 12345, 50000000, 0, {"from": bob} 279 | ) 280 | assert tx.return_value == tx.new_contracts[0] 281 | assert new_factory.pool_count() == 2 282 | 283 | coin = ERC20(decimals=7) 284 | pool = Contract("0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714") 285 | tx = new_factory.deploy_metapool(pool, "Name", "SYM", coin, 123456, 50000000, 0, {"from": bob}) 286 | assert new_factory.pool_count() == 3 287 | 288 | 289 | @pytest.mark.skip 290 | def test_deploy_plain_pool_revert(base_pool, new_factory, new_factory_setup, bob): 291 | coin = ERC20(decimals=7) 292 | new_factory.deploy_metapool(base_pool, "Name", "SYM", coin, 12345, 50000000, 0, {"from": bob}) 293 | existing_coin = base_pool.coins(0) 294 | assert new_factory.base_pool_assets(existing_coin) 295 | coins = [existing_coin, ERC20(decimals=9), ZERO_ADDRESS, ZERO_ADDRESS] 296 | # should revert because a metapool already exists for one of the coins 297 | with brownie.reverts("Invalid asset, deploy a metapool"): 298 | new_factory.deploy_plain_pool("Test Plain", "TST", coins, 12345, 50000000, {"from": bob}) 299 | -------------------------------------------------------------------------------- /tests/test_gauge_manager_proxy.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from brownie import ZERO_ADDRESS 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def manager_proxy(alice, factory, owner_proxy, ManagerProxy): 9 | factory.commit_transfer_ownership(owner_proxy, {"from": alice}) 10 | owner_proxy.accept_transfer_ownership(factory, {"from": alice}) 11 | 12 | proxy = ManagerProxy.deploy(factory, alice, {"from": alice}) 13 | owner_proxy.set_gauge_manager(proxy, {"from": alice}) 14 | return proxy 15 | 16 | 17 | def test_add_reward(alice, manager_proxy, gauge, coin_reward): 18 | assert gauge.reward_count() == 0 19 | manager_proxy.add_reward(gauge, coin_reward, alice, {"from": alice}) 20 | 21 | assert gauge.reward_tokens(0) == coin_reward 22 | assert gauge.reward_data(coin_reward)["distributor"] == alice 23 | 24 | 25 | def test_set_reward_distributor(alice, bob, manager_proxy, gauge, coin_reward): 26 | assert gauge.reward_count() == 0 27 | manager_proxy.add_reward(gauge, coin_reward, alice, {"from": alice}) 28 | 29 | manager_proxy.set_reward_distributor(gauge, coin_reward, bob, {"from": alice}) 30 | assert gauge.reward_data(coin_reward)["distributor"] == bob 31 | 32 | def test_deploy_gauge(alice, manager_proxy, factory, coin_a, coin_b): 33 | pool = factory.deploy_plain_pool( 34 | "foo", 35 | "foo", 36 | [coin_a, coin_b, ZERO_ADDRESS, ZERO_ADDRESS], 37 | 200, 38 | .0004 * 10 ** 10, 39 | 0, 40 | 0, 41 | {"from": alice} 42 | ).return_value 43 | 44 | gauge = manager_proxy.deploy_gauge(pool, {"from": alice}).return_value 45 | 46 | assert manager_proxy.gauge_manager(gauge) == alice 47 | assert factory.get_gauge(pool) == gauge 48 | 49 | 50 | def test_set_gauge_manager(alice, bob, manager_proxy, gauge): 51 | # alice is both the ownership_admin and the manager 52 | tx = manager_proxy.set_gauge_manager(gauge, bob, {"from": alice}) 53 | assert manager_proxy.gauge_manager(gauge) == bob 54 | assert tx.events["SetGaugeManager"].values() == [gauge, bob] 55 | 56 | # bob is the gague manager and can transfer managership 57 | manager_proxy.set_gauge_manager(gauge, alice, {"from": bob}) 58 | assert manager_proxy.gauge_manager(gauge) == alice 59 | 60 | # bob is no longer the gauge manager 61 | with brownie.reverts(): 62 | manager_proxy.set_gauge_manager(gauge, alice, {"from": bob}) 63 | 64 | 65 | def test_set_manager(alice, bob, manager_proxy): 66 | # alice is both the manager and the ownership_admin 67 | manager_proxy.set_manager(bob, {"from": alice}) 68 | assert manager_proxy.manager() == bob 69 | 70 | # now alice is only the ownership_admin 71 | manager_proxy.set_manager(alice, {"from": alice}) 72 | assert manager_proxy.manager() == alice 73 | 74 | with brownie.reverts(): 75 | # bob is not the manager anymore 76 | manager_proxy.set_manager(alice, {"from": bob}) 77 | assert manager_proxy.manager() == alice 78 | -------------------------------------------------------------------------------- /tests/test_migrator.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob", "approve_bob") 5 | 6 | 7 | @pytest.mark.skip 8 | @pytest.fixture(scope="module") 9 | def swap2(MetaUSD, alice, base_pool, factory, coin): 10 | tx = factory.deploy_metapool(base_pool, "Test Swap", "TST", coin, 200, 4000000, {"from": alice}) 11 | yield MetaUSD.at(tx.return_value) 12 | 13 | 14 | @pytest.mark.skip 15 | @pytest.fixture(scope="module") 16 | def migrator(PoolMigrator, alice, swap): 17 | contract = PoolMigrator.deploy({"from": alice}) 18 | swap.approve(contract, 2 ** 256 - 1, {"from": alice}) 19 | yield contract 20 | 21 | 22 | @pytest.mark.skip 23 | def test_migrate(alice, is_rebase, migrator, swap, swap2): 24 | if is_rebase: 25 | pytest.skip() 26 | balance = swap.balanceOf(alice) 27 | migrator.migrate_to_new_pool(swap, swap2, balance, {"from": alice}) 28 | 29 | assert swap.balanceOf(alice) == 0 30 | assert swap2.balanceOf(alice) == balance 31 | 32 | assert swap.balanceOf(migrator) == 0 33 | assert swap2.balanceOf(migrator) == 0 34 | 35 | 36 | @pytest.mark.skip 37 | def test_migrate_partial(alice, is_rebase, migrator, swap, swap2, coin): 38 | if is_rebase: 39 | pytest.skip() 40 | balance = swap.balanceOf(alice) 41 | migrator.migrate_to_new_pool(swap, swap2, balance // 4, {"from": alice}) 42 | 43 | assert swap.balanceOf(alice) == balance - balance // 4 44 | assert abs(swap2.balanceOf(alice) - balance // 4) < 8 + (10 ** 18 - coin.decimals()) 45 | 46 | assert swap.balanceOf(migrator) == 0 47 | assert swap2.balanceOf(migrator) == 0 48 | 49 | 50 | @pytest.mark.skip 51 | def test_migrate_multiple(alice, migrator, is_rebase, swap, swap2, coin): 52 | if is_rebase: 53 | pytest.skip() 54 | balance = swap.balanceOf(alice) 55 | for i in range(4): 56 | migrator.migrate_to_new_pool(swap, swap2, balance // 4, {"from": alice}) 57 | 58 | assert swap.balanceOf(alice) < 5 59 | assert abs(swap2.balanceOf(alice) - balance) < 8 + (10 ** 18 - coin.decimals()) 60 | 61 | assert swap.balanceOf(migrator) == 0 62 | assert swap2.balanceOf(migrator) == 0 63 | 64 | 65 | @pytest.mark.skip 66 | def test_migrate_bidirectional(alice, migrator, is_rebase, swap, swap2): 67 | if is_rebase: 68 | pytest.skip() 69 | balance = swap.balanceOf(alice) 70 | migrator.migrate_to_new_pool(swap, swap2, balance, {"from": alice}) 71 | swap2.approve(migrator, 2 ** 256 - 1, {"from": alice}) 72 | migrator.migrate_to_new_pool(swap2, swap, balance, {"from": alice}) 73 | 74 | assert abs(swap.balanceOf(alice) - balance) < 4 75 | assert swap2.balanceOf(alice) == 0 76 | 77 | assert swap.balanceOf(migrator) == 0 78 | assert swap2.balanceOf(migrator) == 0 79 | 80 | 81 | @pytest.mark.skip 82 | def test_migration_wrong_pool(alice, migrator, swap, swap_btc): 83 | balance = swap.balanceOf(alice) 84 | with brownie.reverts(): 85 | migrator.migrate_to_new_pool(swap, swap_btc, balance, {"from": alice}) 86 | 87 | 88 | @pytest.mark.skip 89 | def test_insufficient_balance(alice, migrator, swap, swap2): 90 | balance = swap.balanceOf(alice) 91 | with brownie.reverts(): 92 | migrator.migrate_to_new_pool(swap, swap2, balance + 1, {"from": alice}) 93 | -------------------------------------------------------------------------------- /tests/test_owner_proxy.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import brownie 4 | import pytest 5 | from brownie import ETH_ADDRESS, ZERO_ADDRESS 6 | from brownie_tokens import ERC20 7 | 8 | 9 | def pack_values(values) -> bytes: 10 | """Stolen from curvefi/curve-pool-registry""" 11 | assert max(values) < 256 12 | return sum(i << c * 8 for c, i in enumerate(values)) 13 | 14 | 15 | @pytest.fixture(autouse=True) 16 | def setup(alice, factory, owner_proxy): 17 | factory.commit_transfer_ownership(owner_proxy, {"from": alice}) 18 | owner_proxy.accept_transfer_ownership(factory, {"from": alice}) 19 | 20 | 21 | @pytest.fixture 22 | def new_factory(Factory, alice): 23 | return Factory.deploy(alice, {"from": alice}) 24 | 25 | 26 | @pytest.fixture 27 | def new_base_pool(alice, CurveTokenV3, CurvePool, registry): 28 | lp_token = CurveTokenV3.deploy("Test LP Token", "Tester", {"from": alice}) 29 | base_coins = [ERC20() for _ in range(3)] 30 | pool = CurvePool.deploy(alice, base_coins, lp_token, 200, 3000000, 5000000000, {"from": alice}) 31 | lp_token.set_minter(pool, {"from": alice}) 32 | 33 | registry.add_pool_without_underlying( 34 | pool, 35 | 3, 36 | lp_token, 37 | "0x0", 38 | pack_values([18, 18, 18]), 39 | pack_values([0, 0, 0]), 40 | True, 41 | False, 42 | "Test Base Pool", 43 | ) 44 | return pool 45 | 46 | 47 | def test_commit_ownership_transfer(factory, owner_proxy, alice, bob): 48 | owner_proxy.commit_transfer_ownership(factory, bob, {"from": alice}) 49 | assert factory.future_admin() == bob 50 | 51 | 52 | def test_only_ownership_admin_commit_ownership_transfer(factory, owner_proxy, bob): 53 | with brownie.reverts(): 54 | owner_proxy.commit_transfer_ownership(factory, bob, {"from": bob}) 55 | 56 | 57 | def test_accept_transfer_ownership(new_factory, alice, owner_proxy): 58 | new_factory.commit_transfer_ownership(owner_proxy, {"from": alice}) 59 | owner_proxy.accept_transfer_ownership(new_factory, {"from": alice}) 60 | assert new_factory.admin() == owner_proxy 61 | 62 | 63 | def test_unguarded_accept_transfer_ownership(new_factory, alice, bob, owner_proxy): 64 | new_factory.commit_transfer_ownership(owner_proxy, {"from": alice}) 65 | owner_proxy.accept_transfer_ownership(new_factory, {"from": bob}) 66 | assert new_factory.admin() == owner_proxy 67 | 68 | 69 | def test_set_fee_receiver(factory, alice, bob, owner_proxy): 70 | # there is no input validation 71 | owner_proxy.set_fee_receiver(factory, ZERO_ADDRESS, bob, {"from": alice}) 72 | assert factory.get_fee_receiver(ZERO_ADDRESS) == bob 73 | 74 | 75 | def test_set_fee_receiver_guarded(factory, bob, owner_proxy): 76 | with brownie.reverts("Access denied"): 77 | owner_proxy.set_fee_receiver(factory, ZERO_ADDRESS, bob, {"from": bob}) 78 | 79 | 80 | def test_commit_new_admins(owner_proxy, alice, bob, charlie, dave): 81 | owner_proxy.commit_set_admins(bob, charlie, dave, {"from": alice}) 82 | assert owner_proxy.future_ownership_admin() == bob 83 | assert owner_proxy.future_parameter_admin() == charlie 84 | assert owner_proxy.future_emergency_admin() == dave 85 | 86 | 87 | def test_guarded_commit_new_admins(owner_proxy, bob): 88 | with brownie.reverts("Access denied"): 89 | owner_proxy.commit_set_admins(bob, bob, bob, {"from": bob}) 90 | 91 | 92 | def test_apply_set_admins(owner_proxy, alice, bob, charlie, dave): 93 | owner_proxy.commit_set_admins(bob, charlie, dave, {"from": alice}) 94 | owner_proxy.apply_set_admins({"from": alice}) 95 | assert owner_proxy.ownership_admin() == bob 96 | assert owner_proxy.parameter_admin() == charlie 97 | assert owner_proxy.emergency_admin() == dave 98 | 99 | 100 | def test_apply_set_admins_guarded(owner_proxy, alice, bob, charlie, dave): 101 | owner_proxy.commit_set_admins(bob, charlie, dave, {"from": alice}) 102 | with brownie.reverts("Access denied"): 103 | owner_proxy.apply_set_admins({"from": bob}) 104 | 105 | 106 | def test_ramp_A(owner_proxy, swap, alice, chain): 107 | future_a = swap.A() * 1.5 108 | future_time = chain.time() + 86400 109 | A_PRECISION = 100 110 | owner_proxy.ramp_A(swap, future_a, future_time, {"from": alice}) 111 | assert swap.future_A() / A_PRECISION == future_a 112 | assert swap.future_A_time() == future_time 113 | chain.sleep(86400 + 1) 114 | 115 | 116 | def test_ramp_A_guarded(owner_proxy, swap, bob): 117 | with brownie.reverts("Access denied"): 118 | owner_proxy.ramp_A(swap, 0, 0, {"from": bob}) 119 | 120 | 121 | def test_stop_ramp_A(owner_proxy, swap, chain, alice): 122 | future_a = swap.A() * 2 123 | future_time = chain.time() + 86400 124 | owner_proxy.ramp_A(swap, future_a, future_time, {"from": alice}) 125 | 126 | chain.sleep(86400 // 2) 127 | owner_proxy.stop_ramp_A(swap, {"from": alice}) 128 | 129 | assert math.isclose(swap.A(), 3 * future_a / 4) 130 | 131 | 132 | def test_stop_ramp_A_guarded(owner_proxy, swap, chain, alice, bob): 133 | future_a = swap.A() * 2 134 | future_time = chain.time() + 86400 135 | owner_proxy.ramp_A(swap, future_a, future_time, {"from": alice}) 136 | 137 | chain.sleep(86400 // 2) 138 | owner_proxy.stop_ramp_A(swap, {"from": alice}) 139 | 140 | with brownie.reverts("Access denied"): 141 | owner_proxy.stop_ramp_A(swap, {"from": bob}) 142 | 143 | 144 | def test_add_base_pool(owner_proxy, factory, new_base_pool, meta_implementations, alice): 145 | 146 | owner_proxy.add_base_pool( 147 | factory, 148 | new_base_pool, 149 | alice, 150 | 2, 151 | meta_implementations + [ZERO_ADDRESS] * 8, 152 | {"from": alice}, 153 | ) 154 | 155 | assert factory.base_pool_list(0) == new_base_pool 156 | 157 | 158 | def test_add_base_pool_guarded(owner_proxy, bob): 159 | with brownie.reverts("Access denied"): 160 | owner_proxy.add_base_pool( 161 | ETH_ADDRESS, ETH_ADDRESS, ETH_ADDRESS, 0, [ZERO_ADDRESS] * 10, {"from": bob} 162 | ) 163 | 164 | 165 | def test_set_metapool_implementations( 166 | alice, owner_proxy, factory, new_base_pool, meta_implementations 167 | ): 168 | impls = meta_implementations + [ZERO_ADDRESS] * 8 169 | owner_proxy.add_base_pool( 170 | factory, 171 | new_base_pool, 172 | ETH_ADDRESS, 173 | 2, 174 | [ZERO_ADDRESS] * 10, 175 | {"from": alice}, 176 | ) 177 | owner_proxy.set_metapool_implementations(factory, new_base_pool, impls, {"from": alice}) 178 | assert factory.metapool_implementations(new_base_pool) == impls 179 | 180 | 181 | def test_set_metapool_implementations_guarded( 182 | bob, owner_proxy, factory, base_pool, meta_implementations 183 | ): 184 | impls = meta_implementations + [ZERO_ADDRESS] * 8 185 | with brownie.reverts("Access denied"): 186 | owner_proxy.set_metapool_implementations(factory, base_pool, impls, {"from": bob}) 187 | 188 | 189 | def test_set_plain_implementation( 190 | alice, owner_proxy, factory, plain_implementations, plain_pool_size 191 | ): 192 | owner_proxy.set_plain_implementations( 193 | factory, plain_pool_size, plain_implementations + [ZERO_ADDRESS] * 6, {"from": alice} 194 | ) 195 | assert factory.plain_implementations(plain_pool_size, 0) == plain_implementations[0] 196 | 197 | 198 | def test_set_plain_implementation_guarded( 199 | bob, owner_proxy, factory, plain_implementations, plain_pool_size 200 | ): 201 | with brownie.reverts("Access denied"): 202 | owner_proxy.set_plain_implementations( 203 | factory, plain_pool_size, plain_implementations + [ZERO_ADDRESS] * 6, {"from": bob} 204 | ) 205 | -------------------------------------------------------------------------------- /tests/token/test_token_approve.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from eip712.messages import EIP712Message 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | @pytest.mark.parametrize("idx", range(5)) 9 | def test_initial_approval_is_zero(swap, alice, accounts, idx): 10 | assert swap.allowance(alice, accounts[idx]) == 0 11 | 12 | 13 | def test_approve(swap, alice, bob): 14 | swap.approve(bob, 10 ** 19, {"from": alice}) 15 | 16 | assert swap.allowance(alice, bob) == 10 ** 19 17 | 18 | 19 | def test_modify_approve_zero_nonzero(swap, alice, bob): 20 | swap.approve(bob, 10 ** 19, {"from": alice}) 21 | swap.approve(bob, 0, {"from": alice}) 22 | swap.approve(bob, 12345678, {"from": alice}) 23 | 24 | assert swap.allowance(alice, bob) == 12345678 25 | 26 | 27 | def test_revoke_approve(swap, alice, bob): 28 | swap.approve(bob, 10 ** 19, {"from": alice}) 29 | swap.approve(bob, 0, {"from": alice}) 30 | 31 | assert swap.allowance(alice, bob) == 0 32 | 33 | 34 | def test_approve_self(swap, alice, bob): 35 | swap.approve(alice, 10 ** 19, {"from": alice}) 36 | 37 | assert swap.allowance(alice, alice) == 10 ** 19 38 | 39 | 40 | def test_only_affects_target(swap, alice, bob): 41 | swap.approve(bob, 10 ** 19, {"from": alice}) 42 | 43 | assert swap.allowance(bob, alice) == 0 44 | 45 | 46 | def test_returns_true(swap, alice, bob): 47 | tx = swap.approve(bob, 10 ** 19, {"from": alice}) 48 | 49 | assert tx.return_value is True 50 | 51 | 52 | def test_approval_event_fires(alice, bob, swap): 53 | tx = swap.approve(bob, 10 ** 19, {"from": alice}) 54 | 55 | assert len(tx.events) == 1 56 | assert tx.events["Approval"].values() == [alice, bob, 10 ** 19] 57 | 58 | 59 | def test_infinite_approval(swap, alice, bob): 60 | swap.approve(bob, 2 ** 256 - 1, {"from": alice}) 61 | swap.transferFrom(alice, bob, 10 ** 18, {"from": bob}) 62 | 63 | assert swap.allowance(alice, bob) == 2 ** 256 - 1 64 | 65 | 66 | def test_permit(accounts, bob, chain, swap, web3): 67 | 68 | alice = accounts.add("0x416b8a7d9290502f5661da81f0cf43893e3d19cb9aea3c426cfb36e8186e9c09") 69 | 70 | class Permit(EIP712Message): 71 | # EIP-712 Domain Fields 72 | _name_: "string" = swap.name() # noqa: F821 73 | _version_: "string" = swap.version() # noqa: F821 74 | _chainId_: "uint256" = chain.id # noqa: F821 75 | _verifyingContract_: "address" = swap.address # noqa: F821 76 | 77 | # EIP-2612 Data Fields 78 | owner: "address" # noqa: F821 79 | spender: "address" # noqa: F821 80 | value: "uint256" # noqa: F821 81 | nonce: "uint256" # noqa: F821 82 | deadline: "uint256" = 2 ** 256 - 1 # noqa: F821 83 | 84 | permit = Permit(owner=alice.address, spender=bob.address, value=2 ** 256 - 1, nonce=0) 85 | sig = alice.sign_message(permit) 86 | 87 | tx = swap.permit(alice, bob, 2 ** 256 - 1, 2 ** 256 - 1, sig.v, sig.r, sig.s, {"from": bob}) 88 | 89 | assert swap.allowance(alice, bob) == 2 ** 256 - 1 90 | assert tx.return_value is True 91 | assert len(tx.events) == 1 92 | assert tx.events["Approval"].values() == [alice.address, bob, 2 ** 256 - 1] 93 | assert swap.nonces(alice) == 1 94 | 95 | 96 | def test_permit_contract(accounts, bob, chain, swap, web3): 97 | 98 | src = """ 99 | @view 100 | @external 101 | def isValidSignature(_hash: bytes32, _sig: Bytes[65]) -> bytes32: 102 | return 0x1626ba7e00000000000000000000000000000000000000000000000000000000 103 | """ 104 | mock_contract = brownie.compile_source(src, vyper_version="0.3.1").Vyper.deploy({"from": bob}) 105 | alice = accounts.add("0x416b8a7d9290502f5661da81f0cf43893e3d19cb9aea3c426cfb36e8186e9c09") 106 | 107 | class Permit(EIP712Message): 108 | # EIP-712 Domain Fields 109 | _name_: "string" = swap.name() # noqa: F821 110 | _version_: "string" = swap.version() # noqa: F821 111 | _chainId_: "uint256" = chain.id # noqa: F821 112 | _verifyingContract_: "address" = swap.address # noqa: F821 113 | 114 | # EIP-2612 Data Fields 115 | owner: "address" # noqa: F821 116 | spender: "address" # noqa: F821 117 | value: "uint256" # noqa: F821 118 | nonce: "uint256" # noqa: F821 119 | deadline: "uint256" = 2 ** 256 - 1 # noqa: F821 120 | 121 | permit = Permit(owner=alice.address, spender=bob.address, value=2 ** 256 - 1, nonce=0) 122 | sig = alice.sign_message(permit) 123 | 124 | tx = swap.permit( 125 | mock_contract, bob, 2 ** 256 - 1, 2 ** 256 - 1, sig.v, sig.r, sig.s, {"from": bob} 126 | ) 127 | 128 | # make sure this is hit when owner is a contract 129 | assert tx.subcalls[-1]["function"] == "isValidSignature(bytes32,bytes)" 130 | -------------------------------------------------------------------------------- /tests/token/test_token_transfer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | def test_sender_balance_decreases(alice, bob, swap): 9 | sender_balance = swap.balanceOf(alice) 10 | amount = sender_balance // 4 11 | 12 | swap.transfer(bob, amount, {"from": alice}) 13 | 14 | assert swap.balanceOf(alice) == sender_balance - amount 15 | 16 | 17 | def test_receiver_balance_increases(alice, bob, swap): 18 | receiver_balance = swap.balanceOf(bob) 19 | amount = swap.balanceOf(alice) // 4 20 | 21 | swap.transfer(bob, amount, {"from": alice}) 22 | 23 | assert swap.balanceOf(bob) == receiver_balance + amount 24 | 25 | 26 | def test_total_supply_not_affected(alice, bob, swap): 27 | total_supply = swap.totalSupply() 28 | amount = swap.balanceOf(alice) 29 | 30 | swap.transfer(bob, amount, {"from": alice}) 31 | 32 | assert swap.totalSupply() == total_supply 33 | 34 | 35 | def test_returns_true(alice, bob, swap): 36 | amount = swap.balanceOf(alice) 37 | tx = swap.transfer(bob, amount, {"from": alice}) 38 | 39 | assert tx.return_value is True 40 | 41 | 42 | def test_transfer_full_balance(alice, bob, swap): 43 | amount = swap.balanceOf(alice) 44 | receiver_balance = swap.balanceOf(bob) 45 | 46 | swap.transfer(bob, amount, {"from": alice}) 47 | 48 | assert swap.balanceOf(alice) == 0 49 | assert swap.balanceOf(bob) == receiver_balance + amount 50 | 51 | 52 | def test_transfer_zero_tokens(alice, bob, swap): 53 | sender_balance = swap.balanceOf(alice) 54 | receiver_balance = swap.balanceOf(bob) 55 | 56 | swap.transfer(bob, 0, {"from": alice}) 57 | 58 | assert swap.balanceOf(alice) == sender_balance 59 | assert swap.balanceOf(bob) == receiver_balance 60 | 61 | 62 | def test_transfer_to_self(alice, bob, swap): 63 | sender_balance = swap.balanceOf(alice) 64 | amount = sender_balance // 4 65 | 66 | swap.transfer(alice, amount, {"from": alice}) 67 | 68 | assert swap.balanceOf(alice) == sender_balance 69 | 70 | 71 | def test_insufficient_balance(alice, bob, swap): 72 | balance = swap.balanceOf(alice) 73 | 74 | with brownie.reverts(): 75 | swap.transfer(bob, balance + 1, {"from": alice}) 76 | 77 | 78 | def test_transfer_event_fires(alice, bob, swap): 79 | amount = swap.balanceOf(alice) 80 | tx = swap.transfer(bob, amount, {"from": alice}) 81 | 82 | assert len(tx.events) == 1 83 | assert tx.events["Transfer"].values() == [alice, bob, amount] 84 | -------------------------------------------------------------------------------- /tests/token/test_token_transferFrom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | import pytest 4 | 5 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity") 6 | 7 | 8 | def test_sender_balance_decreases(alice, bob, charlie, swap): 9 | sender_balance = swap.balanceOf(alice) 10 | amount = sender_balance // 4 11 | 12 | swap.approve(bob, amount, {"from": alice}) 13 | swap.transferFrom(alice, charlie, amount, {"from": bob}) 14 | 15 | assert swap.balanceOf(alice) == sender_balance - amount 16 | 17 | 18 | def test_receiver_balance_increases(alice, bob, charlie, swap): 19 | receiver_balance = swap.balanceOf(charlie) 20 | amount = swap.balanceOf(alice) // 4 21 | 22 | swap.approve(bob, amount, {"from": alice}) 23 | swap.transferFrom(alice, charlie, amount, {"from": bob}) 24 | 25 | assert swap.balanceOf(charlie) == receiver_balance + amount 26 | 27 | 28 | def test_caller_balance_not_affected(alice, bob, charlie, swap): 29 | caller_balance = swap.balanceOf(bob) 30 | amount = swap.balanceOf(alice) 31 | 32 | swap.approve(bob, amount, {"from": alice}) 33 | swap.transferFrom(alice, charlie, amount, {"from": bob}) 34 | 35 | assert swap.balanceOf(bob) == caller_balance 36 | 37 | 38 | def test_caller_approval_affected(alice, bob, charlie, swap): 39 | approval_amount = swap.balanceOf(alice) 40 | transfer_amount = approval_amount // 4 41 | 42 | swap.approve(bob, approval_amount, {"from": alice}) 43 | swap.transferFrom(alice, charlie, transfer_amount, {"from": bob}) 44 | 45 | assert swap.allowance(alice, bob) == approval_amount - transfer_amount 46 | 47 | 48 | def test_receiver_approval_not_affected(alice, bob, charlie, swap): 49 | approval_amount = swap.balanceOf(alice) 50 | transfer_amount = approval_amount // 4 51 | 52 | swap.approve(bob, approval_amount, {"from": alice}) 53 | swap.approve(charlie, approval_amount, {"from": alice}) 54 | swap.transferFrom(alice, charlie, transfer_amount, {"from": bob}) 55 | 56 | assert swap.allowance(alice, charlie) == approval_amount 57 | 58 | 59 | def test_total_supply_not_affected(alice, bob, charlie, swap): 60 | total_supply = swap.totalSupply() 61 | amount = swap.balanceOf(alice) 62 | 63 | swap.approve(bob, amount, {"from": alice}) 64 | swap.transferFrom(alice, charlie, amount, {"from": bob}) 65 | 66 | assert swap.totalSupply() == total_supply 67 | 68 | 69 | def test_returns_true(alice, bob, charlie, swap): 70 | amount = swap.balanceOf(alice) 71 | swap.approve(bob, amount, {"from": alice}) 72 | tx = swap.transferFrom(alice, charlie, amount, {"from": bob}) 73 | 74 | assert tx.return_value is True 75 | 76 | 77 | def test_transfer_full_balance(alice, bob, charlie, swap): 78 | amount = swap.balanceOf(alice) 79 | receiver_balance = swap.balanceOf(charlie) 80 | 81 | swap.approve(bob, amount, {"from": alice}) 82 | swap.transferFrom(alice, charlie, amount, {"from": bob}) 83 | 84 | assert swap.balanceOf(alice) == 0 85 | assert swap.balanceOf(charlie) == receiver_balance + amount 86 | 87 | 88 | def test_transfer_zero_tokens(alice, bob, charlie, swap): 89 | sender_balance = swap.balanceOf(alice) 90 | receiver_balance = swap.balanceOf(charlie) 91 | 92 | swap.approve(bob, sender_balance, {"from": alice}) 93 | swap.transferFrom(alice, charlie, 0, {"from": bob}) 94 | 95 | assert swap.balanceOf(alice) == sender_balance 96 | assert swap.balanceOf(charlie) == receiver_balance 97 | 98 | 99 | def test_transfer_zero_tokens_without_approval(alice, bob, charlie, swap): 100 | sender_balance = swap.balanceOf(alice) 101 | receiver_balance = swap.balanceOf(charlie) 102 | 103 | swap.transferFrom(alice, charlie, 0, {"from": bob}) 104 | 105 | assert swap.balanceOf(alice) == sender_balance 106 | assert swap.balanceOf(charlie) == receiver_balance 107 | 108 | 109 | def test_insufficient_balance(alice, bob, charlie, swap): 110 | balance = swap.balanceOf(alice) 111 | 112 | swap.approve(bob, balance + 1, {"from": alice}) 113 | with brownie.reverts(): 114 | swap.transferFrom(alice, charlie, balance + 1, {"from": bob}) 115 | 116 | 117 | def test_insufficient_approval(alice, bob, charlie, swap): 118 | balance = swap.balanceOf(alice) 119 | 120 | swap.approve(bob, balance - 1, {"from": alice}) 121 | with brownie.reverts(): 122 | swap.transferFrom(alice, charlie, balance, {"from": bob}) 123 | 124 | 125 | def test_no_approval(alice, bob, charlie, swap): 126 | balance = swap.balanceOf(alice) 127 | 128 | with brownie.reverts(): 129 | swap.transferFrom(alice, charlie, balance, {"from": bob}) 130 | 131 | 132 | def test_revoked_approval(alice, bob, charlie, swap): 133 | balance = swap.balanceOf(alice) 134 | 135 | swap.approve(bob, balance, {"from": alice}) 136 | swap.approve(bob, 0, {"from": alice}) 137 | 138 | with brownie.reverts(): 139 | swap.transferFrom(alice, charlie, balance, {"from": bob}) 140 | 141 | 142 | def test_transfer_to_self(alice, bob, swap): 143 | sender_balance = swap.balanceOf(alice) 144 | amount = sender_balance // 4 145 | 146 | swap.approve(alice, sender_balance, {"from": alice}) 147 | swap.transferFrom(alice, alice, amount, {"from": alice}) 148 | 149 | assert swap.balanceOf(alice) == sender_balance 150 | assert swap.allowance(alice, alice) == sender_balance - amount 151 | 152 | 153 | def test_transfer_to_self_no_approval(alice, bob, swap): 154 | amount = swap.balanceOf(alice) 155 | 156 | with brownie.reverts(): 157 | swap.transferFrom(alice, alice, amount, {"from": alice}) 158 | 159 | 160 | def test_transfer_event_fires(alice, bob, charlie, swap): 161 | amount = swap.balanceOf(alice) 162 | 163 | swap.approve(bob, amount, {"from": alice}) 164 | tx = swap.transferFrom(alice, charlie, amount, {"from": bob}) 165 | 166 | assert len(tx.events) == 1 167 | assert tx.events["Transfer"].values() == [alice, charlie, amount] 168 | -------------------------------------------------------------------------------- /tests/zaps/test_add_liquidity_initial_zap.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | pytestmark = pytest.mark.usefixtures("mint_bob_underlying", "approve_zap") 5 | 6 | 7 | def test_lp_token_balances(bob, zap, swap, initial_amounts_underlying): 8 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 9 | 10 | assert 0.9999 < swap.balanceOf(bob) / (2000000 * 10 ** 18) <= 1 11 | assert swap.totalSupply() == swap.balanceOf(bob) 12 | 13 | 14 | def test_underlying_balances(bob, zap, swap, underlying_coins, coins, initial_amounts_underlying): 15 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 16 | 17 | for coin, amount in zip(underlying_coins, initial_amounts_underlying): 18 | assert coin.balanceOf(zap) == 0 19 | if coin in coins: 20 | assert coin.balanceOf(swap) == amount 21 | else: 22 | assert coin.balanceOf(swap) == 0 23 | 24 | 25 | def test_wrapped_balances(bob, zap, swap, coins, initial_amounts_underlying, initial_amounts): 26 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 27 | 28 | for coin, amount in zip(coins, initial_amounts): 29 | assert coin.balanceOf(zap) == 0 30 | assert 0.9999 < coin.balanceOf(swap) / amount <= 1 31 | 32 | 33 | @pytest.mark.parametrize("idx", range(4)) 34 | def test_initial_liquidity_missing_coin(alice, zap, swap, idx, underlying_decimals): 35 | amounts = [10 ** i for i in underlying_decimals] 36 | amounts[idx] = 0 37 | 38 | with brownie.reverts(): 39 | zap.add_liquidity(swap, amounts, 0, {"from": alice}) 40 | -------------------------------------------------------------------------------- /tests/zaps/test_add_liquidity_zap.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_bob_underlying", "approve_zap") 5 | 6 | 7 | def test_lp_token_balances(bob, zap, swap, initial_amounts_underlying): 8 | initial_supply = swap.totalSupply() 9 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 10 | 11 | assert 0.9999 < swap.balanceOf(bob) / (2000000 * 10 ** 18) <= 1 12 | assert 0.9999 < (swap.totalSupply() / 2) / initial_supply <= 1 13 | 14 | 15 | def test_underlying_balances(bob, zap, swap, underlying_coins, coins, initial_amounts_underlying): 16 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 17 | 18 | for coin, amount in zip(underlying_coins, initial_amounts_underlying): 19 | assert coin.balanceOf(zap) == 0 20 | if coin in coins: 21 | assert coin.balanceOf(swap) == amount * 2 22 | else: 23 | assert coin.balanceOf(swap) == 0 24 | 25 | 26 | def test_wrapped_balances(bob, zap, swap, coins, initial_amounts_underlying, initial_amounts): 27 | zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 28 | 29 | for coin, amount in zip(coins, initial_amounts): 30 | assert coin.balanceOf(zap) == 0 31 | assert 0.9999 < coin.balanceOf(swap) / (amount * 2) <= 1 32 | 33 | 34 | @pytest.mark.parametrize("idx", range(4)) 35 | @pytest.mark.parametrize("mod", [0.95, 1.05]) 36 | def test_slippage(bob, swap, zap, underlying_coins, coins, initial_amounts_underlying, idx, mod): 37 | amounts = [i // 10 ** 6 for i in initial_amounts_underlying] 38 | amounts[idx] = int(amounts[idx] * mod) 39 | 40 | zap.add_liquidity(swap, amounts, 0, {"from": bob}) 41 | 42 | for coin in underlying_coins + coins: 43 | assert coin.balanceOf(zap) == 0 44 | 45 | assert swap.balanceOf(zap) == 0 46 | 47 | 48 | @pytest.mark.parametrize("idx", range(4)) 49 | def test_add_one_coin(bob, swap, zap, underlying_coins, coins, underlying_decimals, idx): 50 | 51 | amounts = [0] * len(underlying_decimals) 52 | amounts[idx] = 10 ** underlying_decimals[idx] 53 | zap.add_liquidity(swap, amounts, 0, {"from": bob}) 54 | 55 | for coin in underlying_coins + coins: 56 | assert coin.balanceOf(zap) == 0 57 | 58 | assert swap.balanceOf(zap) == 0 59 | assert 0.999 < swap.balanceOf(bob) / 10 ** 18 < 1 60 | 61 | 62 | def test_insufficient_balance(charlie, swap, zap, underlying_decimals): 63 | amounts = [(10 ** i) for i in underlying_decimals] 64 | with brownie.reverts(): 65 | zap.add_liquidity(swap, amounts, 0, {"from": charlie}) 66 | 67 | 68 | @pytest.mark.parametrize("min_amount", [False, True]) 69 | def test_min_amount_too_high(alice, swap, zap, initial_amounts_underlying, min_amount): 70 | amounts = [i // 10 ** 6 for i in initial_amounts_underlying] 71 | min_amount = 2 * 10 ** 18 + 1 if min_amount else 2 ** 256 - 1 72 | with brownie.reverts(): 73 | zap.add_liquidity(swap, amounts, min_amount, {"from": alice}) 74 | 75 | 76 | def test_min_amount_with_slippage(bob, swap, zap, initial_amounts_underlying): 77 | amounts = [i // 10 ** 6 for i in initial_amounts_underlying] 78 | amounts[0] = int(amounts[0] * 0.99) 79 | amounts[1] = int(amounts[1] * 1.01) 80 | with brownie.reverts(): 81 | zap.add_liquidity(swap, amounts, 2 * 10 ** 18, {"from": bob}) 82 | -------------------------------------------------------------------------------- /tests/zaps/test_receiver_zap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.usefixtures( 4 | "add_initial_liquidity", "mint_bob_underlying", "approve_bob", "approve_zap" 5 | ) 6 | 7 | 8 | def test_add_liquidity(bob, charlie, zap, swap, initial_amounts_underlying): 9 | zap.add_liquidity(swap, initial_amounts_underlying, 0, charlie, {"from": bob}) 10 | 11 | assert swap.balanceOf(charlie) > 0 12 | assert swap.balanceOf(bob) == 0 13 | 14 | 15 | def test_remove_imbalanced( 16 | alice, charlie, dave, zap, swap, initial_amounts_underlying, underlying_coins 17 | ): 18 | amounts = [i // 4 for i in initial_amounts_underlying] 19 | initial_balance = swap.balanceOf(alice) 20 | 21 | swap.transfer(charlie, initial_balance, {"from": alice}) 22 | swap.approve(zap, 2 ** 256 - 1, {"from": charlie}) 23 | zap.remove_liquidity_imbalance(swap, amounts, initial_balance, dave, {"from": charlie}) 24 | 25 | assert swap.balanceOf(charlie) > 0 26 | assert swap.balanceOf(dave) == 0 27 | assert swap.balanceOf(zap) == 0 28 | 29 | for coin in underlying_coins: 30 | assert coin.balanceOf(charlie) == 0 31 | assert coin.balanceOf(dave) > 0 32 | assert coin.balanceOf(zap) == 0 33 | 34 | 35 | def test_remove_one_coin(alice, charlie, dave, zap, underlying_coins, coins, swap): 36 | underlying = underlying_coins[1] 37 | 38 | initial_balance = swap.balanceOf(alice) 39 | amount = initial_balance // 4 40 | 41 | swap.transfer(charlie, initial_balance, {"from": alice}) 42 | swap.approve(zap, 2 ** 256 - 1, {"from": charlie}) 43 | zap.remove_liquidity_one_coin(swap, amount, 1, 0, dave, {"from": charlie}) 44 | 45 | assert underlying.balanceOf(zap) == 0 46 | assert underlying.balanceOf(charlie) == 0 47 | assert underlying.balanceOf(dave) > 0 48 | 49 | assert swap.balanceOf(zap) == 0 50 | assert swap.balanceOf(charlie) + amount == initial_balance 51 | assert swap.balanceOf(dave) == 0 52 | 53 | 54 | def test_remove_liquidity(alice, charlie, dave, zap, swap, underlying_coins): 55 | initial_balance = swap.balanceOf(alice) 56 | withdraw_amount = initial_balance // 3 57 | 58 | swap.transfer(charlie, initial_balance, {"from": alice}) 59 | swap.approve(zap, 2 ** 256 - 1, {"from": charlie}) 60 | zap.remove_liquidity(swap, withdraw_amount, [0, 0, 0, 0], dave, {"from": charlie}) 61 | 62 | assert swap.balanceOf(charlie) > 0 63 | assert swap.balanceOf(dave) == 0 64 | assert swap.balanceOf(zap) == 0 65 | 66 | for coin in underlying_coins: 67 | assert coin.balanceOf(charlie) == 0 68 | assert coin.balanceOf(dave) > 0 69 | assert coin.balanceOf(zap) == 0 70 | -------------------------------------------------------------------------------- /tests/zaps/test_remove_liquidity_imbalance_zap.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(autouse=True) 5 | def setup(alice, bob, swap, add_initial_liquidity, approve_zap): 6 | swap.transfer(bob, swap.balanceOf(alice), {"from": alice}) 7 | 8 | 9 | @pytest.mark.parametrize("divisor", [2, 7, 31337]) 10 | def test_lp_token_balances(bob, zap, swap, divisor, initial_amounts_underlying): 11 | amounts = [i // divisor for i in initial_amounts_underlying] 12 | max_burn = int(((2000000 * 10 ** 18) // divisor) * 1.1) 13 | 14 | initial_balance = swap.balanceOf(bob) 15 | zap.remove_liquidity_imbalance(swap, amounts, max_burn, {"from": bob}) 16 | 17 | # bob is the only LP, total supply is affected in the same way as his balance 18 | assert swap.balanceOf(bob) < initial_balance 19 | assert swap.balanceOf(bob) >= initial_balance - max_burn 20 | 21 | assert swap.balanceOf(zap) == 0 22 | assert swap.balanceOf(bob) == swap.totalSupply() 23 | 24 | 25 | @pytest.mark.parametrize("divisor", [2, 7, 31337]) 26 | def test_wrapped_balances( 27 | bob, 28 | swap, 29 | zap, 30 | underlying_coins, 31 | coins, 32 | initial_amounts, 33 | initial_amounts_underlying, 34 | divisor, 35 | ): 36 | amounts = [i // divisor for i in initial_amounts_underlying] 37 | zap.remove_liquidity_imbalance(swap, amounts, swap.balanceOf(bob), {"from": bob}) 38 | 39 | for coin, initial in zip(coins[1:], initial_amounts[1:]): 40 | assert coin.balanceOf(zap) == 0 41 | assert coin.balanceOf(bob) == 0 42 | 43 | 44 | @pytest.mark.parametrize("divisor", [2, 7, 31337]) 45 | @pytest.mark.parametrize("is_inclusive", [True, False]) 46 | @pytest.mark.parametrize("idx", range(4)) 47 | def test_underlying_balances( 48 | bob, 49 | swap, 50 | zap, 51 | underlying_coins, 52 | coins, 53 | initial_amounts_underlying, 54 | divisor, 55 | idx, 56 | is_inclusive, 57 | ): 58 | if is_inclusive: 59 | amounts = [i // divisor for i in initial_amounts_underlying] 60 | amounts[idx] = 0 61 | else: 62 | amounts = [0] * len(initial_amounts_underlying) 63 | amounts[idx] = initial_amounts_underlying[idx] // divisor 64 | zap.remove_liquidity_imbalance(swap, amounts, swap.balanceOf(bob), {"from": bob}) 65 | 66 | for coin, amount, initial in zip(underlying_coins, amounts, initial_amounts_underlying): 67 | if coin not in coins: 68 | assert coin.balanceOf(swap) == 0 69 | else: 70 | assert coin.balanceOf(swap) == initial - amount 71 | assert coin.balanceOf(zap) == 0 72 | 73 | if amount: 74 | assert 0.9 < coin.balanceOf(bob) / amount <= 1 75 | else: 76 | assert coin.balanceOf(bob) == 0 77 | -------------------------------------------------------------------------------- /tests/zaps/test_remove_liquidity_one_coin_zap.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_zap") 5 | 6 | 7 | @pytest.mark.parametrize("idx", range(4)) 8 | @pytest.mark.parametrize("divisor", [10, 50, 100]) 9 | def test_remove_one(alice, bob, zap, underlying_coins, coins, swap, idx, divisor): 10 | underlying = underlying_coins[idx] 11 | wrapped = coins[min(idx, 1)] 12 | 13 | initial_amount = swap.balanceOf(alice) 14 | amount = initial_amount // divisor 15 | 16 | swap.transfer(bob, initial_amount, {"from": alice}) 17 | zap.remove_liquidity_one_coin(swap, amount, idx, 0, {"from": bob}) 18 | 19 | assert underlying.balanceOf(zap) == 0 20 | assert wrapped.balanceOf(zap) == 0 21 | assert swap.balanceOf(zap) == 0 22 | 23 | if wrapped != underlying: 24 | assert wrapped.balanceOf(bob) == 0 25 | 26 | assert swap.balanceOf(bob) == initial_amount - amount 27 | assert 0 < underlying.balanceOf(bob) <= amount 28 | 29 | 30 | @pytest.mark.parametrize("idx", range(4)) 31 | def test_amount_exceeds_balance(bob, zap, swap, idx): 32 | with brownie.reverts(): 33 | zap.remove_liquidity_one_coin(swap, 1, idx, 0, {"from": bob}) 34 | -------------------------------------------------------------------------------- /tests/zaps/test_remove_liquidity_zap.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_zap") 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(add_initial_liquidity, approve_zap, alice, bob, swap): 9 | swap.transfer(bob, swap.balanceOf(alice), {"from": alice}) 10 | 11 | 12 | @pytest.mark.parametrize("divisor", [1, 23, 1337]) 13 | def test_lp_token_balances(bob, zap, swap, underlying_coins, divisor): 14 | initial_balance = swap.balanceOf(bob) 15 | withdraw_amount = initial_balance // divisor 16 | min_amounts = [0] * len(underlying_coins) 17 | zap.remove_liquidity(swap, withdraw_amount, min_amounts, {"from": bob}) 18 | 19 | # bob is the only LP, total supply is affected in the same way as his balance 20 | assert swap.balanceOf(bob) == initial_balance - withdraw_amount 21 | assert swap.totalSupply() == initial_balance - withdraw_amount 22 | 23 | 24 | @pytest.mark.parametrize("divisor", [1, 23, 1337]) 25 | def test_wrapped_balances( 26 | bob, 27 | swap, 28 | zap, 29 | underlying_coins, 30 | coins, 31 | initial_amounts, 32 | divisor, 33 | ): 34 | initial_balance = swap.balanceOf(bob) 35 | withdraw_amount = initial_balance // divisor 36 | min_amounts = [0] * len(underlying_coins) 37 | zap.remove_liquidity(swap, withdraw_amount, min_amounts, {"from": bob}) 38 | 39 | for coin, initial in zip(coins, initial_amounts): 40 | assert coin.balanceOf(zap) == 0 41 | assert abs(coin.balanceOf(swap) - (initial - (initial // divisor))) <= 1 42 | 43 | 44 | @pytest.mark.parametrize("divisor", [1, 23, 1337]) 45 | def test_underlying_balances( 46 | bob, 47 | swap, 48 | zap, 49 | underlying_coins, 50 | coins, 51 | initial_amounts_underlying, 52 | divisor, 53 | ): 54 | initial_balance = swap.balanceOf(bob) 55 | withdraw_amount = initial_balance // divisor 56 | min_amounts = [0] * len(underlying_coins) 57 | zap.remove_liquidity(swap, withdraw_amount, min_amounts, {"from": bob}) 58 | 59 | for coin in underlying_coins[1:]: 60 | assert coin.balanceOf(zap) == 0 61 | assert coin.balanceOf(swap) == 0 62 | assert coin.balanceOf(bob) > 0 63 | 64 | 65 | @pytest.mark.parametrize("idx", range(4)) 66 | def test_below_min_amount(alice, swap, zap, initial_amounts_underlying, idx): 67 | min_amount = initial_amounts_underlying.copy() 68 | min_amount[idx] += 1 69 | 70 | amount = 2000000 * 10 ** 18 71 | with brownie.reverts(): 72 | zap.remove_liquidity(swap, amount, min_amount, {"from": alice}) 73 | 74 | 75 | def test_amount_exceeds_balance(alice, swap, zap, underlying_coins): 76 | amount = (2000000 * 10 ** 18) + 1 77 | with brownie.reverts(): 78 | zap.remove_liquidity(swap, amount, [0, 0, 0, 0], {"from": alice}) 79 | -------------------------------------------------------------------------------- /tests/zaps/test_return_values.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "approve_zap") 4 | 5 | 6 | def test_remove_liquidity(alice, bob, zap, swap, underlying_coins): 7 | swap.transfer(bob, swap.balanceOf(alice), {"from": alice}) 8 | tx = zap.remove_liquidity(swap, (2000000 * 10 ** 18) // 3, [0, 0, 0, 0], {"from": bob}) 9 | for coin, expected_amount in zip(underlying_coins, tx.return_value): 10 | assert coin.balanceOf(bob) == expected_amount 11 | 12 | 13 | def test_remove_imbalance(alice, bob, zap, initial_amounts_underlying, swap, underlying_coins): 14 | amounts = [i // 2 for i in initial_amounts_underlying] 15 | amounts[0] = 0 16 | 17 | initial_balance = swap.balanceOf(alice) 18 | swap.transfer(bob, initial_balance, {"from": alice}) 19 | tx = zap.remove_liquidity_imbalance(swap, amounts, initial_balance, {"from": bob}) 20 | 21 | assert initial_balance - tx.return_value == swap.balanceOf(bob) 22 | 23 | 24 | def test_remove_one(alice, bob, zap, underlying_coins, coins, swap): 25 | swap.transfer(bob, swap.balanceOf(alice), {"from": alice}) 26 | tx = zap.remove_liquidity_one_coin(swap, 10 ** 18, 1, 0, {"from": bob}) 27 | 28 | assert tx.return_value == underlying_coins[1].balanceOf(bob) 29 | 30 | 31 | def test_add_liquidity(bob, zap, initial_amounts_underlying, swap, mint_bob_underlying): 32 | tx = zap.add_liquidity(swap, initial_amounts_underlying, 0, {"from": bob}) 33 | assert swap.balanceOf(bob) == tx.return_value 34 | --------------------------------------------------------------------------------