├── .gitattributes ├── .github └── workflows │ ├── lint.yaml │ └── main.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── brownie-config.yaml ├── contracts ├── AddressProvider.vy ├── BasePoolRegistry.vy ├── CryptoRegistry.vy ├── CurveCalc.vy ├── PoolInfo.vy ├── Registry.vy ├── Swaps.vy └── testing │ ├── ERC20.vy │ ├── ERC20NoReturn.vy │ ├── ERC20ReturnFalse.vy │ ├── GaugeControllerMock.vy │ ├── LiquidityGaugeMock.vy │ ├── MetaPoolMock.vy │ ├── PoolMockV1.vy │ ├── PoolMockV2.vy │ ├── RateCalcMock.vy │ ├── ankrETH.vy │ ├── cERC20.vy │ └── yERC20.vy ├── package.json ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── scripts ├── add_pools.py ├── deploy.py ├── get_pool_data.py └── utils.py ├── setup.cfg └── tests ├── conftest.py ├── forked ├── conftest.py ├── test_exchange_amounts.py ├── test_exchange_best_rate.py ├── test_exchanges.py ├── test_getters.py └── test_input_amount.py └── local ├── conftest.py ├── integration ├── coin_register_utils.py ├── test_calculator_dxdy.py └── test_simulate_coin_registration.py └── unitary ├── AddressProvider ├── test_add_new_id.py ├── test_admin.py ├── test_set_unset.py └── test_set_unset_registry.py ├── Registry ├── test_add_pool_get_decimals.py ├── test_get_pool_asset_type.py ├── test_get_rate_alt.py ├── test_getters_lending.py ├── test_getters_meta.py ├── test_getters_no_lending.py ├── test_liquidity_gauges.py ├── test_remove_pool_lending.py ├── test_remove_pool_meta.py └── test_remove_pool_no_lending.py └── Swaps ├── test_calculator.py ├── test_exchange.py ├── test_exchange_lending.py ├── test_exchange_meta.py ├── test_get_best_rate.py └── test_token_balance.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.vy linguist-language=Python 2 | -------------------------------------------------------------------------------- /.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 --check scripts tests 23 | 24 | - name: Run flake8 25 | run: flake8 scripts tests 26 | 27 | - name: Run isort 28 | run: isort --check-only --diff --recursive scripts tests 29 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: main workflow 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 | key: compiler-cache 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v1 29 | 30 | - name: Install Ganache 31 | run: npm install 32 | 33 | - name: Setup Python 3.8 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: 3.8 37 | 38 | - name: Install Requirements 39 | run: pip install -r requirements.txt 40 | 41 | - name: Run Tests 42 | run: brownie test tests/local 43 | 44 | coverage: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - name: Cache Compiler Installations 51 | uses: actions/cache@v2 52 | with: 53 | path: | 54 | ~/.solcx 55 | ~/.vvm 56 | key: compiler-cache 57 | 58 | - name: Setup Node.js 59 | uses: actions/setup-node@v1 60 | 61 | - name: Install Ganache 62 | run: npm install 63 | 64 | - name: Setup Python 3.8 65 | uses: actions/setup-python@v2 66 | with: 67 | python-version: 3.8 68 | 69 | - name: Install Requirements 70 | run: pip install -r requirements.txt 71 | 72 | - name: Run Tests 73 | run: brownie test tests/local/unitary --once -C --gas 74 | 75 | # forked: 76 | # runs-on: ubuntu-latest 77 | 78 | # steps: 79 | # - uses: actions/checkout@v2 80 | 81 | # - name: Cache Compiler Installations 82 | # uses: actions/cache@v2 83 | # with: 84 | # path: | 85 | # ~/.solcx 86 | # ~/.vvm 87 | # key: compiler-cache 88 | 89 | # - name: Setup Node.js 90 | # uses: actions/setup-node@v1 91 | 92 | # - name: Install Ganache 93 | # run: npm install 94 | 95 | # - name: Setup Python 3.8 96 | # uses: actions/setup-python@v2 97 | # with: 98 | # python-version: 3.8 99 | 100 | # - name: Install Requirements 101 | # run: pip install -r requirements.txt 102 | 103 | # - name: Run Tests 104 | # run: brownie test tests/forked --network mainnet-fork 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | .venv/ 5 | .vscode/ 6 | build/ 7 | docs/_build 8 | reports/ 9 | venv/ 10 | .ipynb_checkpoints 11 | pooldata.json 12 | .idea 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.10.0 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/pycqa/flake8 7 | rev: 6.0.0 8 | hooks: 9 | - id: flake8 10 | - repo: https://github.com/PyCQA/isort 11 | rev: 5.10.1 12 | hooks: 13 | - id: isort 14 | 15 | default_language_version: 16 | python: python3.8 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Curve.Fi, 2020 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 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # curve-pool-registry 2 | 3 | On-chain registry and unified API for [Curve.fi](https://github.com/curvefi/curve-contract) pools. 4 | 5 | ## Usage 6 | 7 | See the [documentation](https://curve.readthedocs.io/) for information on how this project is organized, and how it may be integrated within other projects. 8 | 9 | ## Deployments 10 | 11 | - [`AddressProvider`](contracts/AddressProvider.vy): [0x0000000022D53366457F9d5E68Ec105046FC4383](https://etherscan.io/address/0x0000000022d53366457f9d5e68ec105046fc4383) 12 | - [`Registry`](contracts/Registry.vy): [0x90e00ace148ca3b23ac1bc8c240c2a7dd9c2d7f5](https://etherscan.io/address/0x90e00ace148ca3b23ac1bc8c240c2a7dd9c2d7f5) 13 | - [`PoolInfo`](contracts/PoolInfo.vy): [0xe64608E223433E8a03a1DaaeFD8Cb638C14B552C](https://etherscan.io/address/0xe64608E223433E8a03a1DaaeFD8Cb638C14B552C) 14 | 15 | ## Testing and Development 16 | 17 | ### Dependencies 18 | 19 | - [python3](https://www.python.org/downloads/release/python-368/) version 3.6 or greater, python3-dev 20 | - [brownie](https://github.com/iamdefinitelyahuman/brownie) - tested with version [1.13.0](https://github.com/eth-brownie/brownie/releases/tag/v1.13.0) 21 | - [brownie-token-tester](https://github.com/iamdefinitelyahuman/brownie-token-tester) - version [0.1.0](https://github.com/iamdefinitelyahuman/brownie-token-tester/releases/tag/v0.1.0) 22 | - [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) 23 | 24 | Curve contracts are compiled using [Vyper](https://github.com/vyperlang/vyper), however installation of the required Vyper versions is handled by Brownie. 25 | 26 | ### Setup 27 | 28 | To get started, first create and initialize a Python [virtual environment](https://docs.python.org/3/library/venv.html). Next, clone the repo and install the developer dependencies: 29 | 30 | ```bash 31 | git clone https://github.com/curvefi/curve-pool-registry.git 32 | cd curve-pool-registry 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | ### Running the Tests 37 | 38 | The registry has two independent test suites. 39 | 40 | #### Local tests 41 | 42 | The [local test suite](tests/local) is designed to be run in a local environment. It is mostly comprised of parametrized unit tests that validate functionality against many possible pool iterations. 43 | 44 | To run the entire local test suite: 45 | 46 | ```bash 47 | brownie test tests/local 48 | ``` 49 | 50 | You can optionally include the `--once` flag to skip parametrization and run each test exactly once. 51 | 52 | #### Forked tests 53 | 54 | The [forked test suite](tests/forked) is designed for use with a forked mainnet. These tests verify functionality within the registry against actual data from deployed pools. The data is obtained from the [`pooldata.json`](https://github.com/curvefi/curve-contract/tree/master/contracts/pools#adding-a-new-pool) file within each subdirectory in [`curvefi/curve-contract/contract/pools`](https://github.com/curvefi/curve-contract/tree/master/contracts/pools). 55 | 56 | To run the forked tests: 57 | 58 | ```bash 59 | brownie test tests/forked 60 | ``` 61 | 62 | You can optionally include the `--pool` flag to only target one or more specific pools: 63 | 64 | ```bash 65 | brownie test tests/forked --pool 3pool,gusd 66 | ``` 67 | 68 | ## Deployment 69 | 70 | Deployment is handled via functions within [`scripts/deploy.py`](scripts/deploy.py). 71 | 72 | To run a deployment function: 73 | 74 | ```bash 75 | brownie run deploy [FUNCTION NAME] --network mainnet 76 | ``` 77 | 78 | You must set `deployer` prior to running on the mainnet. It is recommended to test the script in a forked mainnet environment prior to actual deployment. 79 | 80 | ## License 81 | 82 | Except where otherwise noted, (c) Curve.Fi, 2020 - [All rights reserved](LICENSE). 83 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | reports: 2 | exclude_paths: contracts/testing/* 3 | 4 | autofetch_sources: true 5 | 6 | dependencies: 7 | curvefi/curve-factory@2.0.0 8 | -------------------------------------------------------------------------------- /contracts/AddressProvider.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.7 2 | """ 3 | @title Curve Registry Address Provider 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/BasePoolRegistry.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | """ 3 | @title Curve BasePool Registry 4 | @license MIT 5 | @author Curve.Fi 6 | """ 7 | MAX_COINS: constant(uint256) = 8 8 | 9 | 10 | struct BasePool: 11 | location: uint256 12 | lp_token: address 13 | n_coins: uint256 14 | is_v2: bool 15 | is_legacy: bool 16 | is_lending: bool 17 | 18 | 19 | interface AddressProvider: 20 | def admin() -> address: view 21 | 22 | 23 | interface ERC20: 24 | def decimals() -> uint256: view 25 | 26 | 27 | interface CurvePoolLegacy: 28 | def coins(i: int128) -> address: view 29 | 30 | 31 | interface CurvePool: 32 | def coins(i: uint256) -> address: view 33 | 34 | 35 | event BasePoolAdded: 36 | basepool: indexed(address) 37 | 38 | 39 | event BasePoolRemoved: 40 | basepool: indexed(address) 41 | 42 | 43 | ADDRESS_PROVIDER: constant(address) = 0x0000000022D53366457F9d5E68Ec105046FC4383 44 | base_pool: HashMap[address, BasePool] 45 | base_pool_list: public(address[100]) 46 | get_base_pool_for_lp_token: public(HashMap[address, address]) 47 | base_pool_count: public(uint256) 48 | last_updated: public(uint256) 49 | 50 | 51 | @internal 52 | @view 53 | def _get_basepool_coins(_pool: address) -> address[MAX_COINS]: 54 | _n_coins: uint256 = self.base_pool[_pool].n_coins 55 | _is_legacy: bool = self.base_pool[_pool].is_legacy 56 | _coins: address[MAX_COINS] = empty(address[MAX_COINS]) 57 | for i in range(MAX_COINS): 58 | if i == _n_coins: 59 | break 60 | 61 | if _is_legacy: 62 | _coins[i] = CurvePoolLegacy(_pool).coins(convert(i, int128)) 63 | else: 64 | _coins[i] = CurvePool(_pool).coins(i) 65 | 66 | return _coins 67 | 68 | 69 | 70 | @internal 71 | @view 72 | def _get_basepools_for_coin(_coin: address) -> DynArray[address, 1000]: 73 | """ 74 | @notice Gets the base pool for a coin 75 | @dev Some coins can be in multiple base pools, this function returns 76 | the base pool for a coin at a specific index 77 | @param _coin Address of the coin 78 | @return basepool addresses 79 | """ 80 | _base_pools: DynArray[address, 1000] = empty(DynArray[address, 1000]) 81 | for _pool in self.base_pool_list: 82 | _coins: address[MAX_COINS] = self._get_basepool_coins(_pool) 83 | if _coin in _coins: 84 | _base_pools.append(_pool) 85 | 86 | return _base_pools 87 | 88 | 89 | @external 90 | @view 91 | def get_coins(_pool: address) -> address[MAX_COINS]: 92 | """ 93 | @notice Gets coins in a base pool 94 | @param _pool Address of the base pool 95 | @return address[MAX_COINS] with coin addresses 96 | """ 97 | return self._get_basepool_coins(_pool) 98 | 99 | 100 | @external 101 | @view 102 | def get_basepool_for_coin(_coin: address, _idx: uint256 = 0) -> address: 103 | """ 104 | @notice Gets the base pool for a coin 105 | @dev Some coins can be in multiple base pools, this function returns 106 | the base pool for a coin at a specific index 107 | @param _coin Address of the coin 108 | @param _idx Index of base pool that holds the coin 109 | @return basepool address 110 | """ 111 | return self._get_basepools_for_coin(_coin)[_idx] 112 | 113 | 114 | @external 115 | @view 116 | def get_basepools_for_coin(_coin: address) -> DynArray[address, 1000]: 117 | """ 118 | @notice Gets the base pool for a coin 119 | @dev Some coins can be in multiple base pools, this function returns 120 | the base pool for a coin at a specific index 121 | @param _coin Address of the coin 122 | @return basepool addresses 123 | """ 124 | return self._get_basepools_for_coin(_coin) 125 | 126 | 127 | @external 128 | @view 129 | def get_decimals(_pool: address) -> uint256[MAX_COINS]: 130 | """ 131 | @notice Gets decimals of coins in a base pool 132 | @param _pool Address of the base pool 133 | @return uint256[MAX_COINS] containing coin decimals 134 | """ 135 | _coins: address[MAX_COINS] = self._get_basepool_coins(_pool) 136 | _decimals: uint256[MAX_COINS] = empty(uint256[MAX_COINS]) 137 | for i in range(MAX_COINS): 138 | if _coins[i] == empty(address): 139 | break 140 | if _coins[i] == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 141 | _decimals[i] = 18 142 | else: 143 | _decimals[i] = ERC20(_coins[i]).decimals() 144 | 145 | return _decimals 146 | 147 | 148 | @external 149 | @view 150 | def get_lp_token(_pool: address) -> address: 151 | """ 152 | @notice Gets the LP token of a base pool 153 | @param _pool Address of the base pool 154 | @return address of the LP token 155 | """ 156 | return self.base_pool[_pool].lp_token 157 | 158 | 159 | @external 160 | @view 161 | def get_n_coins(_pool: address) -> uint256: 162 | """ 163 | @notice Gets the number of coins in a base pool 164 | @param _pool Address of the base pool 165 | @return uint256 number of coins 166 | """ 167 | return self.base_pool[_pool].n_coins 168 | 169 | 170 | @external 171 | @view 172 | def get_location(_pool: address) -> uint256: 173 | """ 174 | @notice Gets the index where a base pool's 175 | data is stored in the registry 176 | @param _pool Address of the base pool 177 | @return uint256 index of the base pool 178 | """ 179 | return self.base_pool[_pool].location 180 | 181 | 182 | @external 183 | @view 184 | def is_legacy(_pool: address) -> bool: 185 | """ 186 | @notice Checks if a base pool uses Curve's legacy abi 187 | @dev Legacy abi includes int128 indices whereas the newer 188 | abi uses uint256 indices 189 | @param _pool Address of the base pool 190 | @return bool True if legacy abi is used 191 | """ 192 | return self.base_pool[_pool].is_legacy 193 | 194 | 195 | @external 196 | @view 197 | def is_v2(_pool: address) -> bool: 198 | """ 199 | @notice Checks if a base pool is a Curve CryptoSwap pool 200 | @param _pool Address of the base pool 201 | @return bool True if the pool is a Curve CryptoSwap pool 202 | """ 203 | return self.base_pool[_pool].is_v2 204 | 205 | 206 | @external 207 | @view 208 | def is_lending(_pool: address) -> bool: 209 | """ 210 | @notice Checks if a base pool is a Curve Lending pool 211 | @param _pool Address of the base pool 212 | @return bool True if the pool is a Curve Lending pool 213 | """ 214 | return self.base_pool[_pool].is_lending 215 | 216 | 217 | @external 218 | def add_base_pool(_pool: address, _lp_token: address, _n_coins: uint256, _is_legacy: bool, _is_lending: bool, _is_v2: bool): 219 | """ 220 | @notice Add a base pool to the registry 221 | @param _pool Address of the base pool 222 | @param _lp_token Address of the LP token 223 | @param _n_coins Number of coins in the base pool 224 | @param _is_legacy True if the base pool uses legacy abi 225 | @param _is_lending True if the base pool is a Curve Lending pool 226 | @param _is_v2 True if the base pool is a Curve CryptoSwap pool 227 | """ 228 | assert msg.sender == AddressProvider(ADDRESS_PROVIDER).admin() # dev: admin-only function 229 | assert self.base_pool[_pool].lp_token == empty(address) # dev: pool exists 230 | 231 | # add pool to base_pool_list 232 | base_pool_count: uint256 = self.base_pool_count 233 | self.base_pool[_pool].location = base_pool_count 234 | self.base_pool[_pool].lp_token = _lp_token 235 | self.base_pool[_pool].n_coins = _n_coins 236 | self.base_pool[_pool].is_v2 = _is_v2 237 | self.base_pool[_pool].is_legacy = _is_legacy 238 | self.base_pool[_pool].is_lending = _is_lending 239 | 240 | # for reverse lookup: 241 | self.get_base_pool_for_lp_token[_lp_token] = _pool 242 | 243 | self.last_updated = block.timestamp 244 | self.base_pool_list[base_pool_count] = _pool 245 | self.base_pool_count = base_pool_count + 1 246 | log BasePoolAdded(_pool) 247 | 248 | 249 | @external 250 | def remove_base_pool(_pool: address): 251 | """ 252 | @notice Remove a base pool from the registry 253 | @param _pool Address of the base pool 254 | """ 255 | assert msg.sender == AddressProvider(ADDRESS_PROVIDER).admin() # dev: admin-only function 256 | assert _pool != empty(address) 257 | assert self.base_pool[_pool].lp_token != empty(address) # dev: pool doesn't exist 258 | 259 | # reset pool <> lp_token mappings 260 | self.get_base_pool_for_lp_token[self.base_pool[_pool].lp_token] = empty(address) 261 | self.base_pool[_pool].lp_token = empty(address) 262 | self.base_pool[_pool].n_coins = 0 263 | 264 | # remove base_pool from base_pool_list 265 | location: uint256 = self.base_pool[_pool].location 266 | length: uint256 = self.base_pool_count - 1 267 | assert location < length 268 | 269 | # because self.base_pool_list is a static array, 270 | # we can replace the last index with empty(address) 271 | # and replace the first index with the base pool 272 | # that was previously in the last index. 273 | # we skip this step if location == last index 274 | if location < length: 275 | # replace _pool with final value in pool_list 276 | addr: address = self.base_pool_list[length] 277 | assert addr != empty(address) 278 | self.base_pool_list[location] = addr 279 | self.base_pool[addr].location = location 280 | 281 | # delete final pool_list value 282 | self.base_pool_list[length] = empty(address) 283 | self.base_pool_count = length 284 | 285 | self.last_updated = block.timestamp 286 | log BasePoolRemoved(_pool) 287 | -------------------------------------------------------------------------------- /contracts/CurveCalc.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.7 2 | """ 3 | @title Curve Registry Calculator 4 | @license (c) Curve.Fi, 2020 5 | @author Curve.Fi 6 | @notice Stateless bulk calculator of prices for stablecoin-to-stablecoin pools 7 | """ 8 | 9 | MAX_COINS: constant(int128) = 8 10 | INPUT_SIZE: constant(int128) = 100 11 | FEE_DENOMINATOR: constant(uint256) = 10 ** 10 12 | 13 | 14 | @pure 15 | @internal 16 | def get_D(n_coins: uint256, xp: uint256[MAX_COINS], amp: uint256) -> uint256: 17 | """ 18 | @notice Calculating the invariant (D) 19 | @param n_coins Number of coins in the pool 20 | @param xp Array with coin balances made into the same (1e18) digits 21 | @param amp Amplification coefficient 22 | @return The value of invariant 23 | """ 24 | S: uint256 = 0 25 | for _x in xp: 26 | if _x == 0: 27 | break 28 | S += _x 29 | if S == 0: 30 | return 0 31 | 32 | Dprev: uint256 = 0 33 | D: uint256 = S 34 | Ann: uint256 = amp * n_coins 35 | for _i in range(255): 36 | D_P: uint256 = D 37 | for _x in xp: 38 | if _x == 0: 39 | break 40 | D_P = D_P * D / (_x * n_coins) # If division by 0, this will be borked: only withdrawal will work. And that is good 41 | Dprev = D 42 | D = (Ann * S + D_P * n_coins) * D / ((Ann - 1) * D + (n_coins + 1) * D_P) 43 | # Equality with the precision of 1 44 | if D > Dprev: 45 | if D - Dprev <= 1: 46 | break 47 | else: 48 | if Dprev - D <= 1: 49 | break 50 | return D 51 | 52 | 53 | @pure 54 | @internal 55 | def get_y(D: uint256, n_coins: uint256, xp: uint256[MAX_COINS], amp: uint256, 56 | i: int128, j: int128, x: uint256) -> uint256: 57 | """ 58 | @notice Bulk-calculate new balance of coin j given a new value of coin i 59 | @param D The Invariant 60 | @param n_coins Number of coins in the pool 61 | @param xp Array with coin balances made into the same (1e18) digits 62 | @param amp Amplification coefficient 63 | @param i Index of the changed coin (trade in) 64 | @param j Index of the other changed coin (trade out) 65 | @param x Amount of coin i (trade in) 66 | @return Amount of coin j (trade out) 67 | """ 68 | n_coins_int: int128 = convert(n_coins, int128) 69 | assert (i != j) and (i >= 0) and (j >= 0) and (i < n_coins_int) and (j < n_coins_int) 70 | 71 | Ann: uint256 = amp * n_coins 72 | 73 | _x: uint256 = 0 74 | S_: uint256 = 0 75 | c: uint256 = D 76 | for _i in range(MAX_COINS): 77 | if _i == n_coins_int: 78 | break 79 | if _i == i: 80 | _x = x 81 | elif _i != j: 82 | _x = xp[_i] 83 | else: 84 | continue 85 | S_ += _x 86 | c = c * D / (_x * n_coins) 87 | c = c * D / (Ann * n_coins) 88 | b: uint256 = S_ + D / Ann # - D 89 | y_prev: uint256 = 0 90 | y: uint256 = D 91 | for _i in range(255): 92 | y_prev = y 93 | y = (y*y + c) / (2 * y + b - D) 94 | # Equality with the precision of 1 95 | if y > y_prev: 96 | if y - y_prev <= 1: 97 | break 98 | else: 99 | if y_prev - y <= 1: 100 | break 101 | 102 | return y 103 | 104 | 105 | @view 106 | @external 107 | def get_dy(n_coins: uint256, balances: uint256[MAX_COINS], amp: uint256, fee: uint256, 108 | rates: uint256[MAX_COINS], precisions: uint256[MAX_COINS], 109 | i: int128, j: int128, dx: uint256[INPUT_SIZE]) -> uint256[INPUT_SIZE]: 110 | """ 111 | @notice Bulk-calculate amount of of coin j given in exchange for coin i 112 | @param n_coins Number of coins in the pool 113 | @param balances Array with coin balances 114 | @param amp Amplification coefficient 115 | @param fee Pool's fee at 1e10 basis 116 | @param rates Array with rates for "lent out" tokens 117 | @param precisions Precision multipliers to get the coin to 1e18 basis 118 | @param i Index of the changed coin (trade in) 119 | @param j Index of the other changed coin (trade out) 120 | @param dx Array of values of coin i (trade in) 121 | @return Array of values of coin j (trade out) 122 | """ 123 | 124 | xp: uint256[MAX_COINS] = balances 125 | ratesp: uint256[MAX_COINS] = precisions 126 | for k in range(MAX_COINS): 127 | xp[k] = xp[k] * rates[k] * precisions[k] / 10 ** 18 128 | ratesp[k] *= rates[k] 129 | D: uint256 = self.get_D(n_coins, xp, amp) 130 | 131 | dy: uint256[INPUT_SIZE] = dx 132 | for k in range(INPUT_SIZE): 133 | if dx[k] == 0: 134 | break 135 | else: 136 | x_after_trade: uint256 = dx[k] * ratesp[i] / 10 ** 18 + xp[i] 137 | dy[k] = self.get_y(D, n_coins, xp, amp, i, j, x_after_trade) 138 | dy[k] = (xp[j] - dy[k] - 1) * 10 ** 18 / ratesp[j] 139 | dy[k] -= dy[k] * fee / FEE_DENOMINATOR 140 | 141 | return dy 142 | 143 | 144 | @view 145 | @external 146 | def get_dx(n_coins: uint256, balances: uint256[MAX_COINS], amp: uint256, fee: uint256, 147 | rates: uint256[MAX_COINS], precisions: uint256[MAX_COINS], 148 | i: int128, j: int128, dy: uint256) -> uint256: 149 | """ 150 | @notice Calculate amount of of coin i taken when exchanging for coin j 151 | @param n_coins Number of coins in the pool 152 | @param balances Array with coin balances 153 | @param amp Amplification coefficient 154 | @param fee Pool's fee at 1e10 basis 155 | @param rates Array with rates for "lent out" tokens 156 | @param precisions Precision multipliers to get the coin to 1e18 basis 157 | @param i Index of the changed coin (trade in) 158 | @param j Index of the other changed coin (trade out) 159 | @param dy Amount of coin j (trade out) 160 | @return Amount of coin i (trade in) 161 | """ 162 | 163 | xp: uint256[MAX_COINS] = balances 164 | ratesp: uint256[MAX_COINS] = precisions 165 | for k in range(MAX_COINS): 166 | xp[k] = xp[k] * rates[k] * precisions[k] / 10 ** 18 167 | ratesp[k] *= rates[k] 168 | D: uint256 = self.get_D(n_coins, xp, amp) 169 | 170 | y_after_trade: uint256 = xp[j] - dy * ratesp[j] / 10 ** 18 * FEE_DENOMINATOR / (FEE_DENOMINATOR - fee) 171 | x: uint256 = self.get_y(D, n_coins, xp, amp, j, i, y_after_trade) 172 | dx: uint256 = (x - xp[i]) * 10 ** 18 / ratesp[i] 173 | 174 | return dx 175 | -------------------------------------------------------------------------------- /contracts/PoolInfo.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.7 2 | """ 3 | @title Curve Registry PoolInfo 4 | @license MIT 5 | @author Curve.Fi 6 | @notice Large getters designed for off-chain use 7 | """ 8 | 9 | MAX_COINS: constant(int128) = 8 10 | 11 | 12 | interface AddressProvider: 13 | def get_registry() -> address: view 14 | def admin() -> address: view 15 | 16 | interface Registry: 17 | def get_coins(_pool: address) -> address[MAX_COINS]: view 18 | def get_underlying_coins(_pool: address) -> address[MAX_COINS]: view 19 | def get_decimals(_pool: address) -> uint256[MAX_COINS]: view 20 | def get_underlying_decimals(_pool: address) -> uint256[MAX_COINS]: view 21 | def get_balances(_pool: address) -> uint256[MAX_COINS]: view 22 | def get_underlying_balances(_pool: address) -> uint256[MAX_COINS]: view 23 | def get_rates(_pool: address) -> uint256[MAX_COINS]: view 24 | def get_lp_token(_pool: address) -> address: view 25 | def get_parameters(_pool: address) -> PoolParams: view 26 | def is_meta(_pool: address) -> bool: view 27 | def get_pool_name(_pool: address) -> String[64]: view 28 | 29 | 30 | struct PoolParams: 31 | A: uint256 32 | future_A: uint256 33 | fee: uint256 34 | admin_fee: uint256 35 | future_fee: uint256 36 | future_admin_fee: uint256 37 | future_owner: address 38 | initial_A: uint256 39 | initial_A_time: uint256 40 | future_A_time: uint256 41 | 42 | struct PoolInfo: 43 | balances: uint256[MAX_COINS] 44 | underlying_balances: uint256[MAX_COINS] 45 | decimals: uint256[MAX_COINS] 46 | underlying_decimals: uint256[MAX_COINS] 47 | rates: uint256[MAX_COINS] 48 | lp_token: address 49 | params: PoolParams 50 | is_meta: bool 51 | name: String[64] 52 | 53 | struct PoolCoins: 54 | coins: address[MAX_COINS] 55 | underlying_coins: address[MAX_COINS] 56 | decimals: uint256[MAX_COINS] 57 | underlying_decimals: uint256[MAX_COINS] 58 | 59 | 60 | address_provider: public(AddressProvider) 61 | 62 | 63 | @external 64 | def __init__(_provider: address): 65 | self.address_provider = AddressProvider(_provider) 66 | 67 | 68 | @view 69 | @external 70 | def get_pool_coins(_pool: address) -> PoolCoins: 71 | """ 72 | @notice Get information on coins in a pool 73 | @dev Empty values in the returned arrays may be ignored 74 | @param _pool Pool address 75 | @return Coin addresses, underlying coin addresses, underlying coin decimals 76 | """ 77 | registry: address = self.address_provider.get_registry() 78 | 79 | return PoolCoins({ 80 | coins: Registry(registry).get_coins(_pool), 81 | underlying_coins: Registry(registry).get_underlying_coins(_pool), 82 | decimals: Registry(registry).get_decimals(_pool), 83 | underlying_decimals: Registry(registry).get_underlying_decimals(_pool), 84 | }) 85 | 86 | 87 | @view 88 | @external 89 | def get_pool_info(_pool: address) -> PoolInfo: 90 | """ 91 | @notice Get information on a pool 92 | @dev Reverts if the pool address is unknown 93 | @param _pool Pool address 94 | @return balances, underlying balances, decimals, underlying decimals, 95 | lp token, amplification coefficient, fees 96 | """ 97 | registry: address = self.address_provider.get_registry() 98 | 99 | return PoolInfo({ 100 | balances: Registry(registry).get_balances(_pool), 101 | underlying_balances: Registry(registry).get_underlying_balances(_pool), 102 | decimals: Registry(registry).get_decimals(_pool), 103 | underlying_decimals: Registry(registry).get_underlying_decimals(_pool), 104 | rates: Registry(registry).get_rates(_pool), 105 | lp_token: Registry(registry).get_lp_token(_pool), 106 | params: Registry(registry).get_parameters(_pool), 107 | is_meta: Registry(registry).is_meta(_pool), 108 | name: Registry(registry).get_pool_name(_pool), 109 | }) 110 | -------------------------------------------------------------------------------- /contracts/testing/ERC20.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(_to: address, _value: uint256): 69 | self.total_supply += _value 70 | self.balanceOf[_to] += _value 71 | log Transfer(ZERO_ADDRESS, _to, _value) 72 | -------------------------------------------------------------------------------- /contracts/testing/ERC20NoReturn.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock non-standard ERC20 for testing 4 | @dev transfer and approve functions return None rather than True 5 | """ 6 | 7 | event Transfer: 8 | _from: indexed(address) 9 | _to: indexed(address) 10 | _value: uint256 11 | 12 | event Approval: 13 | _owner: indexed(address) 14 | _spender: indexed(address) 15 | _value: uint256 16 | 17 | 18 | name: public(String[64]) 19 | symbol: public(String[32]) 20 | decimals: public(uint256) 21 | balanceOf: public(HashMap[address, uint256]) 22 | allowances: HashMap[address, HashMap[address, uint256]] 23 | total_supply: uint256 24 | 25 | 26 | @external 27 | def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): 28 | self.name = _name 29 | self.symbol = _symbol 30 | self.decimals = _decimals 31 | 32 | 33 | @external 34 | @view 35 | def totalSupply() -> uint256: 36 | return self.total_supply 37 | 38 | 39 | @external 40 | @view 41 | def allowance(_owner : address, _spender : address) -> uint256: 42 | return self.allowances[_owner][_spender] 43 | 44 | 45 | @external 46 | def transfer(_to : address, _value : uint256): 47 | self.balanceOf[msg.sender] -= _value 48 | self.balanceOf[_to] += _value 49 | log Transfer(msg.sender, _to, _value) 50 | 51 | 52 | @external 53 | def transferFrom(_from : address, _to : address, _value : uint256): 54 | self.balanceOf[_from] -= _value 55 | self.balanceOf[_to] += _value 56 | self.allowances[_from][msg.sender] -= _value 57 | log Transfer(_from, _to, _value) 58 | 59 | 60 | @external 61 | def approve(_spender : address, _value : uint256): 62 | self.allowances[msg.sender][_spender] = _value 63 | log Approval(msg.sender, _spender, _value) 64 | 65 | 66 | @external 67 | def _mint_for_testing(_to: address, _value: uint256): 68 | self.total_supply += _value 69 | self.balanceOf[_to] += _value 70 | log Transfer(ZERO_ADDRESS, _to, _value) 71 | -------------------------------------------------------------------------------- /contracts/testing/ERC20ReturnFalse.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | 3 | """ 4 | @notice Mock non-standard ERC20 for testing 5 | @dev failed transfers return False instead of reverting 6 | """ 7 | 8 | event Transfer: 9 | _from: indexed(address) 10 | _to: indexed(address) 11 | _value: uint256 12 | 13 | event Approval: 14 | _owner: indexed(address) 15 | _spender: indexed(address) 16 | _value: uint256 17 | 18 | 19 | name: public(String[64]) 20 | symbol: public(String[32]) 21 | decimals: public(uint256) 22 | balanceOf: public(HashMap[address, uint256]) 23 | allowances: HashMap[address, HashMap[address, uint256]] 24 | total_supply: uint256 25 | 26 | 27 | @external 28 | def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): 29 | self.name = _name 30 | self.symbol = _symbol 31 | self.decimals = _decimals 32 | 33 | 34 | @external 35 | @view 36 | def totalSupply() -> uint256: 37 | return self.total_supply 38 | 39 | 40 | @external 41 | @view 42 | def allowance(_owner : address, _spender : address) -> uint256: 43 | return self.allowances[_owner][_spender] 44 | 45 | 46 | @external 47 | def transfer(_to : address, _value : uint256) -> bool: 48 | if self.balanceOf[msg.sender] < _value: 49 | return False 50 | 51 | self.balanceOf[msg.sender] -= _value 52 | self.balanceOf[_to] += _value 53 | log Transfer(msg.sender, _to, _value) 54 | return True 55 | 56 | 57 | @external 58 | def transferFrom(_from : address, _to : address, _value : uint256)-> bool: 59 | if self.balanceOf[_from] < _value: 60 | return False 61 | if self.allowances[_from][msg.sender] < _value: 62 | return False 63 | 64 | self.balanceOf[_from] -= _value 65 | self.balanceOf[_to] += _value 66 | self.allowances[_from][msg.sender] -= _value 67 | log Transfer(_from, _to, _value) 68 | return True 69 | 70 | 71 | @external 72 | def approve(_spender : address, _value : uint256): 73 | self.allowances[msg.sender][_spender] = _value 74 | log Approval(msg.sender, _spender, _value) 75 | 76 | 77 | @external 78 | def _mint_for_testing(_to: address, _value: uint256): 79 | self.total_supply += _value 80 | self.balanceOf[_to] += _value 81 | log Transfer(ZERO_ADDRESS, _to, _value) 82 | -------------------------------------------------------------------------------- /contracts/testing/GaugeControllerMock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.4 2 | 3 | gauge_types_: HashMap[address, int128] 4 | 5 | 6 | @view 7 | @external 8 | def gauge_types(_gauge: address) -> int128: 9 | assert self.gauge_types_[_gauge] != 0 10 | return self.gauge_types_[_gauge] - 1 11 | 12 | 13 | @external 14 | def _set_gauge_type(_gauge: address, _type: int128): 15 | self.gauge_types_[_gauge] = _type + 1 16 | -------------------------------------------------------------------------------- /contracts/testing/LiquidityGaugeMock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.4 2 | 3 | 4 | lp_token: public(address) 5 | 6 | @external 7 | def __init__(_lp_token: address): 8 | self.lp_token = _lp_token 9 | -------------------------------------------------------------------------------- /contracts/testing/MetaPoolMock.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock Curve metapool for testing 4 | """ 5 | 6 | FEE_PRECISION: constant(uint256) = 10**10 7 | 8 | interface ERC20Mock: 9 | def decimals() -> uint256: view 10 | def balanceOf(_addr: address) -> uint256: view 11 | def transfer(_to: address, _amount: uint256) -> bool: nonpayable 12 | def transferFrom(_from: address, _to: address, _amount: uint256) -> bool: nonpayable 13 | def _mint_for_testing(_to: address, _amount: uint256): nonpayable 14 | 15 | n_coins: public(uint256) 16 | n_coins_underlying: uint256 17 | coin_list: address[4] 18 | base_coin_list: address[4] 19 | 20 | A: public(uint256) 21 | initial_A: public(uint256) 22 | initial_A_time: public(uint256) 23 | future_A: public(uint256) 24 | future_A_time: public(uint256) 25 | 26 | fee: public(uint256) 27 | admin_fee: public(uint256) 28 | future_fee: public(uint256) 29 | future_admin_fee: public(uint256) 30 | future_owner: public(address) 31 | 32 | base_pool: public(address) 33 | get_virtual_price: public(uint256) 34 | 35 | _balances: uint256[4] 36 | 37 | @external 38 | def __init__( 39 | _n_coins: uint256, 40 | _n_coins_underlying: uint256, 41 | _base_pool: address, 42 | _coin_list: address[4], 43 | _base_coin_list: address[4], 44 | _A: uint256, 45 | _fee: uint256, 46 | ): 47 | self.n_coins = _n_coins 48 | self.n_coins_underlying = _n_coins_underlying 49 | self.base_pool = _base_pool 50 | self.coin_list = _coin_list 51 | self.base_coin_list = _base_coin_list 52 | self.A = _A 53 | self.fee = _fee 54 | self.get_virtual_price = 10**18 55 | 56 | 57 | @external 58 | @view 59 | def coins(i: uint256) -> address: 60 | assert i < self.n_coins # dev: exceeds n_coins 61 | return self.coin_list[i] 62 | 63 | 64 | @external 65 | @view 66 | def base_coins(i: uint256) -> address: 67 | assert i < self.n_coins_underlying # dev: exceeds n_coins 68 | return self.base_coin_list[i] 69 | 70 | 71 | @external 72 | @view 73 | def balances(i: uint256) -> uint256: 74 | assert i < self.n_coins 75 | return self._balances[i] 76 | 77 | 78 | @internal 79 | @view 80 | def _get_dy(_from: address, _to: address, _dx: uint256) -> uint256: 81 | _from_precision: uint256 = 0 82 | _to_precision: uint256 = 0 83 | 84 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 85 | _from_precision = 18 86 | else: 87 | _from_precision = ERC20Mock(_from).decimals() 88 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 89 | _to_precision = 18 90 | else: 91 | _to_precision = ERC20Mock(_to).decimals() 92 | 93 | _dy: uint256 = _dx * (10**_to_precision) / (10**_from_precision) 94 | _fee: uint256 = _dy * self.fee / FEE_PRECISION 95 | 96 | return _dy - _fee 97 | 98 | 99 | @external 100 | @view 101 | def get_dy(i: int128, j: int128, dx: uint256) -> uint256: 102 | return self._get_dy(self.coin_list[i], self.coin_list[j], dx) 103 | 104 | 105 | @external 106 | @view 107 | def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: 108 | _from: address = ZERO_ADDRESS 109 | _to: address = ZERO_ADDRESS 110 | 111 | max_base_coin: int128 = convert(self.n_coins - 1, int128) 112 | if i <= max_base_coin: 113 | _from = self.coin_list[i] 114 | else: 115 | _from = self.base_coin_list[i-max_base_coin] 116 | if j <= max_base_coin: 117 | _to = self.coin_list[j] 118 | else: 119 | _to = self.base_coin_list[j-max_base_coin] 120 | 121 | return self._get_dy(_from, _to, dx) 122 | 123 | 124 | @internal 125 | def _exchange(_sender: address, _from: address, _to: address, dx: uint256, min_dy: uint256): 126 | dy: uint256 = self._get_dy(_from, _to, dx) 127 | assert dy >= min_dy, "Exchange resulted in fewer coins than expected" 128 | 129 | if _from != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 130 | _response: Bytes[32] = raw_call( 131 | _from, 132 | concat( 133 | method_id("transferFrom(address,address,uint256)"), 134 | convert(_sender, bytes32), 135 | convert(self, bytes32), 136 | convert(dx, bytes32), 137 | ), 138 | max_outsize=32, 139 | ) 140 | if len(_response) > 0: 141 | assert convert(_response, bool) 142 | 143 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 144 | send(_sender, dy) 145 | else: 146 | ERC20Mock(_to)._mint_for_testing(self, dy) 147 | _response: Bytes[32] = raw_call( 148 | _to, 149 | concat( 150 | method_id("transfer(address,uint256)"), 151 | convert(_sender, bytes32), 152 | convert(dy, bytes32), 153 | ), 154 | max_outsize=32, 155 | ) 156 | if len(_response) > 0: 157 | assert convert(_response, bool) 158 | 159 | 160 | @external 161 | @payable 162 | @nonreentrant('lock') 163 | def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): 164 | _from: address = self.coin_list[i] 165 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 166 | assert msg.value == dx 167 | else: 168 | assert msg.value == 0 169 | 170 | self._exchange(msg.sender, _from, self.coin_list[j], dx, min_dy) 171 | 172 | 173 | @external 174 | @payable 175 | @nonreentrant('lock') 176 | def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256): 177 | _from: address = ZERO_ADDRESS 178 | _to: address = ZERO_ADDRESS 179 | 180 | max_base_coin: int128 = convert(self.n_coins - 1, int128) 181 | if i < max_base_coin: 182 | _from = self.coin_list[i] 183 | else: 184 | _from = self.base_coin_list[i-max_base_coin] 185 | if j < max_base_coin: 186 | _to = self.coin_list[j] 187 | else: 188 | _to = self.base_coin_list[j-max_base_coin] 189 | 190 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 191 | assert msg.value == dx 192 | else: 193 | assert msg.value == 0 194 | 195 | self._exchange(msg.sender, _from, _to, dx, min_dy) 196 | 197 | 198 | # testing functions 199 | 200 | @external 201 | def _set_A( 202 | _A: uint256, 203 | _initial_A: uint256, 204 | _initial_A_time: uint256, 205 | _future_A: uint256, 206 | _future_A_time: uint256 207 | ): 208 | self.A = _A 209 | self.initial_A = _initial_A 210 | self.initial_A_time = _initial_A_time 211 | self.future_A = _future_A 212 | self.future_A_time = _future_A_time 213 | 214 | 215 | @external 216 | def _set_fees_and_owner( 217 | _fee: uint256, 218 | _admin_fee: uint256, 219 | _future_fee: uint256, 220 | _future_admin_fee: uint256, 221 | _future_owner: address 222 | ): 223 | self.fee = _fee 224 | self.admin_fee = _admin_fee 225 | self.future_fee = _future_fee 226 | self.future_admin_fee = _future_admin_fee 227 | self.future_owner = _future_owner 228 | 229 | @external 230 | def _set_balances(_new_balances: uint256[4]): 231 | self._balances = _new_balances 232 | 233 | 234 | @external 235 | def _set_virtual_price(_value: uint256): 236 | self.get_virtual_price = _value 237 | 238 | 239 | @external 240 | @payable 241 | def __default__(): 242 | pass 243 | -------------------------------------------------------------------------------- /contracts/testing/PoolMockV1.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock Curve pool for testing 4 | """ 5 | 6 | FEE_PRECISION: constant(uint256) = 10**10 7 | 8 | interface ERC20Mock: 9 | def decimals() -> uint256: view 10 | def balanceOf(_addr: address) -> uint256: view 11 | def transfer(_to: address, _amount: uint256) -> bool: nonpayable 12 | def transferFrom(_from: address, _to: address, _amount: uint256) -> bool: nonpayable 13 | def _mint_for_testing(_to: address, _amount: uint256): nonpayable 14 | 15 | n_coins: int128 16 | coin_list: address[4] 17 | underlying_coin_list: address[4] 18 | 19 | A: public(uint256) 20 | initial_A: public(uint256) 21 | initial_A_time: public(uint256) 22 | future_A: public(uint256) 23 | future_A_time: public(uint256) 24 | 25 | fee: public(uint256) 26 | admin_fee: public(uint256) 27 | future_fee: public(uint256) 28 | future_admin_fee: public(uint256) 29 | future_owner: public(address) 30 | get_virtual_price: public(uint256) 31 | 32 | _balances: uint256[4] 33 | 34 | @external 35 | def __init__( 36 | _n_coins: int128, 37 | _coin_list: address[4], 38 | _underlying_coin_list: address[4], 39 | _A: uint256, 40 | _fee: uint256, 41 | ): 42 | self.n_coins = _n_coins 43 | self.coin_list = _coin_list 44 | self.underlying_coin_list = _underlying_coin_list 45 | self.A = _A 46 | self.fee = _fee 47 | self.get_virtual_price = 10**18 48 | 49 | 50 | @external 51 | @view 52 | def coins(i: int128) -> address: 53 | assert i < self.n_coins # dev: exceeds n_coins 54 | return self.coin_list[i] 55 | 56 | 57 | @external 58 | @view 59 | def underlying_coins(i: int128) -> address: 60 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 61 | assert i < self.n_coins # dev: exceeds n_coins 62 | return self.underlying_coin_list[i] 63 | 64 | 65 | @external 66 | @view 67 | def balances(i: int128) -> uint256: 68 | assert i < self.n_coins 69 | return self._balances[i] 70 | 71 | 72 | @internal 73 | @view 74 | def _get_dy(_from: address, _to: address, _dx: uint256) -> uint256: 75 | _from_precision: uint256 = 0 76 | _to_precision: uint256 = 0 77 | 78 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 79 | _from_precision = 18 80 | else: 81 | _from_precision = ERC20Mock(_from).decimals() 82 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 83 | _to_precision = 18 84 | else: 85 | _to_precision = ERC20Mock(_to).decimals() 86 | 87 | _dy: uint256 = _dx * (10**_to_precision) / (10**_from_precision) 88 | _fee: uint256 = _dy * self.fee / FEE_PRECISION 89 | 90 | return _dy - _fee 91 | 92 | 93 | @external 94 | @view 95 | def get_dy(i: int128, j: int128, dx: uint256) -> uint256: 96 | return self._get_dy(self.coin_list[i], self.coin_list[j], dx) 97 | 98 | 99 | @external 100 | @view 101 | def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: 102 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 103 | return self._get_dy(self.underlying_coin_list[i], self.underlying_coin_list[j], dx) 104 | 105 | 106 | @internal 107 | def _exchange(_sender: address, _from: address, _to: address, dx: uint256, min_dy: uint256): 108 | dy: uint256 = self._get_dy(_from, _to, dx) 109 | assert dy >= min_dy, "Exchange resulted in fewer coins than expected" 110 | 111 | if _from != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 112 | _response: Bytes[32] = raw_call( 113 | _from, 114 | concat( 115 | method_id("transferFrom(address,address,uint256)"), 116 | convert(_sender, bytes32), 117 | convert(self, bytes32), 118 | convert(dx, bytes32), 119 | ), 120 | max_outsize=32, 121 | ) 122 | if len(_response) > 0: 123 | assert convert(_response, bool) 124 | 125 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 126 | send(_sender, dy) 127 | else: 128 | ERC20Mock(_to)._mint_for_testing(self, dy) 129 | _response: Bytes[32] = raw_call( 130 | _to, 131 | concat( 132 | method_id("transfer(address,uint256)"), 133 | convert(_sender, bytes32), 134 | convert(dy, bytes32), 135 | ), 136 | max_outsize=32, 137 | ) 138 | if len(_response) > 0: 139 | assert convert(_response, bool) 140 | 141 | 142 | @external 143 | @payable 144 | @nonreentrant('lock') 145 | def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): 146 | _from: address = self.coin_list[i] 147 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 148 | assert msg.value == dx 149 | else: 150 | assert msg.value == 0 151 | 152 | self._exchange(msg.sender, _from, self.coin_list[j], dx, min_dy) 153 | 154 | 155 | @external 156 | @payable 157 | @nonreentrant('lock') 158 | def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256): 159 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 160 | 161 | _from: address = self.underlying_coin_list[i] 162 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 163 | assert msg.value == dx 164 | else: 165 | assert msg.value == 0 166 | 167 | self._exchange(msg.sender, _from, self.underlying_coin_list[j], dx, min_dy) 168 | 169 | 170 | # testing functions 171 | 172 | @external 173 | def _set_A( 174 | _A: uint256, 175 | _initial_A: uint256, 176 | _initial_A_time: uint256, 177 | _future_A: uint256, 178 | _future_A_time: uint256 179 | ): 180 | self.A = _A 181 | self.initial_A = _initial_A 182 | self.initial_A_time = _initial_A_time 183 | self.future_A = _future_A 184 | self.future_A_time = _future_A_time 185 | 186 | 187 | @external 188 | def _set_fees_and_owner( 189 | _fee: uint256, 190 | _admin_fee: uint256, 191 | _future_fee: uint256, 192 | _future_admin_fee: uint256, 193 | _future_owner: address 194 | ): 195 | self.fee = _fee 196 | self.admin_fee = _admin_fee 197 | self.future_fee = _future_fee 198 | self.future_admin_fee = _future_admin_fee 199 | self.future_owner = _future_owner 200 | 201 | 202 | @external 203 | def _set_balances(_new_balances: uint256[4]): 204 | self._balances = _new_balances 205 | 206 | 207 | @external 208 | def _set_virtual_price(_value: uint256): 209 | self.get_virtual_price = _value 210 | 211 | 212 | @external 213 | @payable 214 | def __default__(): 215 | pass 216 | -------------------------------------------------------------------------------- /contracts/testing/PoolMockV2.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock Curve pool for testing 4 | """ 5 | 6 | FEE_PRECISION: constant(uint256) = 10**10 7 | 8 | interface ERC20Mock: 9 | def decimals() -> uint256: view 10 | def balanceOf(_addr: address) -> uint256: view 11 | def transfer(_to: address, _amount: uint256) -> bool: nonpayable 12 | def transferFrom(_from: address, _to: address, _amount: uint256) -> bool: nonpayable 13 | def _mint_for_testing(_to: address, _amount: uint256): nonpayable 14 | 15 | n_coins: uint256 16 | coin_list: address[4] 17 | underlying_coin_list: address[4] 18 | 19 | A: public(uint256) 20 | initial_A: public(uint256) 21 | initial_A_time: public(uint256) 22 | future_A: public(uint256) 23 | future_A_time: public(uint256) 24 | 25 | fee: public(uint256) 26 | admin_fee: public(uint256) 27 | future_fee: public(uint256) 28 | future_admin_fee: public(uint256) 29 | future_owner: public(address) 30 | get_virtual_price: public(uint256) 31 | 32 | _balances: uint256[4] 33 | 34 | @external 35 | def __init__( 36 | _n_coins: uint256, 37 | _coin_list: address[4], 38 | _underlying_coin_list: address[4], 39 | _A: uint256, 40 | _fee: uint256, 41 | ): 42 | self.n_coins = _n_coins 43 | self.coin_list = _coin_list 44 | self.underlying_coin_list = _underlying_coin_list 45 | self.A = _A 46 | self.fee = _fee 47 | self.get_virtual_price = 10**18 48 | 49 | 50 | @external 51 | @view 52 | def coins(i: uint256) -> address: 53 | assert i < self.n_coins # dev: exceeds n_coins 54 | return self.coin_list[i] 55 | 56 | 57 | @external 58 | @view 59 | def underlying_coins(i: uint256) -> address: 60 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 61 | assert i < self.n_coins # dev: exceeds n_coins 62 | return self.underlying_coin_list[i] 63 | 64 | 65 | @external 66 | @view 67 | def balances(i: uint256) -> uint256: 68 | assert i < self.n_coins 69 | return self._balances[i] 70 | 71 | 72 | @internal 73 | @view 74 | def _get_dy(_from: address, _to: address, _dx: uint256) -> uint256: 75 | _from_precision: uint256 = 0 76 | _to_precision: uint256 = 0 77 | 78 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 79 | _from_precision = 18 80 | else: 81 | _from_precision = ERC20Mock(_from).decimals() 82 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 83 | _to_precision = 18 84 | else: 85 | _to_precision = ERC20Mock(_to).decimals() 86 | 87 | _dy: uint256 = _dx * (10**_to_precision) / (10**_from_precision) 88 | _fee: uint256 = _dy * self.fee / FEE_PRECISION 89 | 90 | return _dy - _fee 91 | 92 | 93 | @external 94 | @view 95 | def get_dy(i: int128, j: int128, dx: uint256) -> uint256: 96 | return self._get_dy(self.coin_list[i], self.coin_list[j], dx) 97 | 98 | 99 | @external 100 | @view 101 | def get_dy_underlying(i: int128, j: int128, dx: uint256) -> uint256: 102 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 103 | return self._get_dy(self.underlying_coin_list[i], self.underlying_coin_list[j], dx) 104 | 105 | 106 | @internal 107 | def _exchange(_sender: address, _from: address, _to: address, dx: uint256, min_dy: uint256): 108 | dy: uint256 = self._get_dy(_from, _to, dx) 109 | assert dy >= min_dy, "Exchange resulted in fewer coins than expected" 110 | 111 | if _from != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 112 | _response: Bytes[32] = raw_call( 113 | _from, 114 | concat( 115 | method_id("transferFrom(address,address,uint256)"), 116 | convert(_sender, bytes32), 117 | convert(self, bytes32), 118 | convert(dx, bytes32), 119 | ), 120 | max_outsize=32, 121 | ) 122 | if len(_response) > 0: 123 | assert convert(_response, bool) 124 | 125 | if _to == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 126 | send(_sender, dy) 127 | else: 128 | ERC20Mock(_to)._mint_for_testing(self, dy) 129 | _response: Bytes[32] = raw_call( 130 | _to, 131 | concat( 132 | method_id("transfer(address,uint256)"), 133 | convert(_sender, bytes32), 134 | convert(dy, bytes32), 135 | ), 136 | max_outsize=32, 137 | ) 138 | if len(_response) > 0: 139 | assert convert(_response, bool) 140 | 141 | 142 | @external 143 | @payable 144 | @nonreentrant('lock') 145 | def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256): 146 | _from: address = self.coin_list[i] 147 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 148 | assert msg.value == dx 149 | else: 150 | assert msg.value == 0 151 | 152 | self._exchange(msg.sender, _from, self.coin_list[j], dx, min_dy) 153 | 154 | 155 | @external 156 | @payable 157 | @nonreentrant('lock') 158 | def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256): 159 | assert self.underlying_coin_list[0] != ZERO_ADDRESS 160 | 161 | _from: address = self.underlying_coin_list[i] 162 | if _from == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: 163 | assert msg.value == dx 164 | else: 165 | assert msg.value == 0 166 | 167 | self._exchange(msg.sender, _from, self.underlying_coin_list[j], dx, min_dy) 168 | 169 | 170 | # testing functions 171 | 172 | @external 173 | def _set_A( 174 | _A: uint256, 175 | _initial_A: uint256, 176 | _initial_A_time: uint256, 177 | _future_A: uint256, 178 | _future_A_time: uint256 179 | ): 180 | self.A = _A 181 | self.initial_A = _initial_A 182 | self.initial_A_time = _initial_A_time 183 | self.future_A = _future_A 184 | self.future_A_time = _future_A_time 185 | 186 | 187 | @external 188 | def _set_fees_and_owner( 189 | _fee: uint256, 190 | _admin_fee: uint256, 191 | _future_fee: uint256, 192 | _future_admin_fee: uint256, 193 | _future_owner: address 194 | ): 195 | self.fee = _fee 196 | self.admin_fee = _admin_fee 197 | self.future_fee = _future_fee 198 | self.future_admin_fee = _future_admin_fee 199 | self.future_owner = _future_owner 200 | 201 | 202 | @external 203 | def _set_balances(_new_balances: uint256[4]): 204 | self._balances = _new_balances 205 | 206 | 207 | @external 208 | def _set_virtual_price(_value: uint256): 209 | self.get_virtual_price = _value 210 | 211 | 212 | @external 213 | @payable 214 | def __default__(): 215 | pass 216 | -------------------------------------------------------------------------------- /contracts/testing/RateCalcMock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.11 2 | """ 3 | @title Mock Curve Pool Rate Calculator 4 | """ 5 | 6 | 7 | @view 8 | @external 9 | def get_rate(_coin: address) -> uint256: 10 | result: Bytes[32] = raw_call(_coin, 0x71ca337d, max_outsize=32, is_static_call=True) 11 | return 10 ** 36 / convert(result, uint256) 12 | -------------------------------------------------------------------------------- /contracts/testing/ankrETH.vy: -------------------------------------------------------------------------------- 1 | # @version 0.2.8 2 | """ 3 | @notice Mock ankrETH contract for testing registry rate getter 4 | """ 5 | 6 | 7 | ratio: public(uint256) 8 | 9 | 10 | @external 11 | def __init__(): 12 | self.ratio = 10 ** 18 13 | 14 | 15 | @external 16 | def update_ratio(new_ratio: uint256): 17 | assert new_ratio < self.ratio 18 | self.ratio = new_ratio 19 | -------------------------------------------------------------------------------- /contracts/testing/cERC20.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | """ 3 | @notice Mock cERC20 for testing 4 | """ 5 | 6 | from vyper.interfaces import ERC20 7 | 8 | event Transfer: 9 | _from: indexed(address) 10 | _to: indexed(address) 11 | _value: uint256 12 | 13 | event Approval: 14 | _owner: indexed(address) 15 | _spender: indexed(address) 16 | _value: uint256 17 | 18 | 19 | name: public(String[64]) 20 | symbol: public(String[32]) 21 | decimals: public(uint256) 22 | balanceOf: public(HashMap[address, uint256]) 23 | allowances: HashMap[address, HashMap[address, uint256]] 24 | total_supply: uint256 25 | 26 | underlying_token: ERC20 27 | exchangeRateStored: public(uint256) 28 | supplyRatePerBlock: public(uint256) 29 | accrualBlockNumber: public(uint256) 30 | 31 | @external 32 | def __init__( 33 | _name: String[64], 34 | _symbol: String[32], 35 | _decimals: uint256, 36 | _underlying_token: address, 37 | _exchange_rate: uint256 38 | ): 39 | self.name = _name 40 | self.symbol = _symbol 41 | self.decimals = _decimals 42 | self.underlying_token = ERC20(_underlying_token) 43 | self.exchangeRateStored = _exchange_rate 44 | self.accrualBlockNumber = block.number 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 | # cERC20-specific functions 84 | @external 85 | def mint(mintAmount: uint256) -> uint256: 86 | """ 87 | @notice Sender supplies assets into the market and receives cTokens in exchange 88 | @dev Accrues interest whether or not the operation succeeds, unless reverted 89 | @param mintAmount The amount of the underlying asset to supply 90 | @return uint 0=success, otherwise a failure 91 | """ 92 | self.underlying_token.transferFrom(msg.sender, self, mintAmount) 93 | value: uint256 = mintAmount * 10 ** 18 / self.exchangeRateStored 94 | self.total_supply += value 95 | self.balanceOf[msg.sender] += value 96 | return 0 97 | 98 | 99 | @external 100 | def redeem(redeemTokens: uint256) -> uint256: 101 | """ 102 | @notice Sender redeems cTokens in exchange for the underlying asset 103 | @dev Accrues interest whether or not the operation succeeds, unless reverted 104 | @param redeemTokens The number of cTokens to redeem into underlying 105 | @return uint 0=success, otherwise a failure 106 | """ 107 | uvalue: uint256 = redeemTokens * self.exchangeRateStored / 10 ** 18 108 | self.balanceOf[msg.sender] -= redeemTokens 109 | self.total_supply -= redeemTokens 110 | self.underlying_token.transfer(msg.sender, uvalue) 111 | return 0 112 | 113 | 114 | @external 115 | def redeemUnderlying(redeemAmount: uint256) -> uint256: 116 | """ 117 | @notice Sender redeems cTokens in exchange for a specified amount of underlying asset 118 | @dev Accrues interest whether or not the operation succeeds, unless reverted 119 | @param redeemAmount The amount of underlying to redeem 120 | @return uint 0=success, otherwise a failure 121 | """ 122 | value: uint256 = redeemAmount * 10 ** 18 / self.exchangeRateStored 123 | self.balanceOf[msg.sender] -= value 124 | self.total_supply -= value 125 | self.underlying_token.transfer(msg.sender, redeemAmount) 126 | return 0 127 | 128 | 129 | @external 130 | def exchangeRateCurrent() -> uint256: 131 | _rate: uint256 = self.exchangeRateStored 132 | self.exchangeRateStored = _rate # Simulate blockchain write 133 | return _rate 134 | 135 | 136 | # testing methods 137 | @external 138 | def _set_exchange_rate(_rate: uint256): 139 | self.exchangeRateStored = _rate 140 | 141 | 142 | @external 143 | def _mint_for_testing(_to: address, _value: uint256): 144 | self.total_supply += _value 145 | self.balanceOf[_to] += _value 146 | log Transfer(ZERO_ADDRESS, _to, _value) 147 | -------------------------------------------------------------------------------- /contracts/testing/yERC20.vy: -------------------------------------------------------------------------------- 1 | # @version ^0.2.0 2 | 3 | """ 4 | @notice Mock ERC20 for testing 5 | """ 6 | 7 | from vyper.interfaces import ERC20 8 | 9 | event Transfer: 10 | _from: indexed(address) 11 | _to: indexed(address) 12 | _value: uint256 13 | 14 | event Approval: 15 | _owner: indexed(address) 16 | _spender: indexed(address) 17 | _value: uint256 18 | 19 | 20 | name: public(String[64]) 21 | symbol: public(String[32]) 22 | decimals: public(uint256) 23 | balanceOf: public(HashMap[address, uint256]) 24 | allowances: HashMap[address, HashMap[address, uint256]] 25 | total_supply: uint256 26 | 27 | underlying_token: ERC20 28 | getPricePerFullShare: public(uint256) 29 | 30 | 31 | @external 32 | def __init__( 33 | _name: String[64], 34 | _symbol: String[32], 35 | _decimals: uint256, 36 | _underlying_token: address, 37 | _exchange_rate: uint256 38 | ): 39 | self.name = _name 40 | self.symbol = _symbol 41 | self.decimals = _decimals 42 | self.underlying_token = ERC20(_underlying_token) 43 | self.getPricePerFullShare = _exchange_rate 44 | 45 | 46 | @external 47 | @view 48 | def totalSupply() -> uint256: 49 | return self.total_supply 50 | 51 | 52 | @external 53 | @view 54 | def allowance(_owner : address, _spender : address) -> uint256: 55 | return self.allowances[_owner][_spender] 56 | 57 | 58 | @external 59 | def transfer(_to : address, _value : uint256) -> bool: 60 | self.balanceOf[msg.sender] -= _value 61 | self.balanceOf[_to] += _value 62 | log Transfer(msg.sender, _to, _value) 63 | return True 64 | 65 | 66 | @external 67 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 68 | self.balanceOf[_from] -= _value 69 | self.balanceOf[_to] += _value 70 | self.allowances[_from][msg.sender] -= _value 71 | log Transfer(_from, _to, _value) 72 | return True 73 | 74 | 75 | @external 76 | def approve(_spender : address, _value : uint256) -> bool: 77 | self.allowances[msg.sender][_spender] = _value 78 | log Approval(msg.sender, _spender, _value) 79 | return True 80 | 81 | 82 | # yERC20-specific functions 83 | @external 84 | def deposit(depositAmount: uint256): 85 | """ 86 | @notice Sender supplies assets into the market and receives yTokens in exchange 87 | @dev Accrues interest whether or not the operation succeeds, unless reverted 88 | @param depositAmount The amount of the underlying asset to supply 89 | """ 90 | self.underlying_token.transferFrom(msg.sender, self, depositAmount) 91 | value: uint256 = depositAmount * 10 ** 18 / self.getPricePerFullShare 92 | self.total_supply += value 93 | self.balanceOf[msg.sender] += value 94 | 95 | 96 | @external 97 | def withdraw(withdrawTokens: uint256): 98 | """ 99 | @notice Sender redeems yTokens in exchange for the underlying asset 100 | @dev Accrues interest whether or not the operation succeeds, unless reverted 101 | @param withdrawTokens The number of yTokens to redeem into underlying 102 | """ 103 | uvalue: uint256 = withdrawTokens * self.getPricePerFullShare / 10 ** 18 104 | self.balanceOf[msg.sender] -= withdrawTokens 105 | self.total_supply -= withdrawTokens 106 | self.underlying_token.transfer(msg.sender, uvalue) 107 | 108 | 109 | # testing functions 110 | @external 111 | def _set_exchange_rate(_rate: uint256): 112 | self.getPricePerFullShare = _rate 113 | 114 | 115 | @external 116 | def _mint_for_testing(_to: address, _value: uint256): 117 | self.total_supply += _value 118 | self.balanceOf[_to] += _value 119 | log Transfer(ZERO_ADDRESS, _to, _value) 120 | -------------------------------------------------------------------------------- /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.in: -------------------------------------------------------------------------------- 1 | black 2 | brownie-token-tester 3 | eth-brownie 4 | flake8 5 | isort 6 | pre-commit 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.8 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | aiohttp==3.8.3 8 | # via 9 | # eth-brownie 10 | # web3 11 | aiosignal==1.2.0 12 | # via 13 | # aiohttp 14 | # eth-brownie 15 | asttokens==2.0.5 16 | # via 17 | # eth-brownie 18 | # vyper 19 | async-timeout==4.0.2 20 | # via 21 | # aiohttp 22 | # eth-brownie 23 | attrs==22.1.0 24 | # via 25 | # aiohttp 26 | # eth-brownie 27 | # hypothesis 28 | # jsonschema 29 | # pytest 30 | base58==2.1.1 31 | # via 32 | # eth-brownie 33 | # multiaddr 34 | bitarray==2.6.0 35 | # via 36 | # eth-account 37 | # eth-brownie 38 | black==22.10.0 39 | # via 40 | # -r requirements.in 41 | # eth-brownie 42 | brownie-token-tester==0.3.2 43 | # via -r requirements.in 44 | certifi==2022.9.24 45 | # via 46 | # eth-brownie 47 | # requests 48 | cfgv==3.3.1 49 | # via pre-commit 50 | charset-normalizer==2.1.1 51 | # via 52 | # aiohttp 53 | # eth-brownie 54 | # requests 55 | click==8.1.3 56 | # via 57 | # black 58 | # eth-brownie 59 | cytoolz==0.12.0 60 | # via 61 | # eth-brownie 62 | # eth-keyfile 63 | # eth-utils 64 | dataclassy==0.11.1 65 | # via 66 | # eip712 67 | # eth-brownie 68 | distlib==0.3.6 69 | # via virtualenv 70 | eip712==0.1.0 71 | # via eth-brownie 72 | eth-abi==2.2.0 73 | # via 74 | # eip712 75 | # eth-account 76 | # eth-brownie 77 | # eth-event 78 | # web3 79 | eth-account==0.5.9 80 | # via 81 | # eth-brownie 82 | # web3 83 | eth-brownie==1.19.2 84 | # via 85 | # -r requirements.in 86 | # brownie-token-tester 87 | eth-event==1.2.3 88 | # via eth-brownie 89 | eth-hash[pycryptodome]==0.3.3 90 | # via 91 | # eth-brownie 92 | # eth-event 93 | # eth-utils 94 | # web3 95 | eth-keyfile==0.5.1 96 | # via 97 | # eth-account 98 | # eth-brownie 99 | eth-keys==0.3.4 100 | # via 101 | # eth-account 102 | # eth-brownie 103 | # eth-keyfile 104 | eth-rlp==0.2.1 105 | # via 106 | # eth-account 107 | # eth-brownie 108 | # web3 109 | eth-typing==2.3.0 110 | # via 111 | # eip712 112 | # eth-abi 113 | # eth-brownie 114 | # eth-keys 115 | # eth-utils 116 | # web3 117 | eth-utils==1.10.0 118 | # via 119 | # eip712 120 | # eth-abi 121 | # eth-account 122 | # eth-brownie 123 | # eth-event 124 | # eth-keyfile 125 | # eth-keys 126 | # eth-rlp 127 | # rlp 128 | # web3 129 | execnet==1.9.0 130 | # via 131 | # eth-brownie 132 | # pytest-xdist 133 | filelock==3.8.2 134 | # via virtualenv 135 | flake8==6.0.0 136 | # via -r requirements.in 137 | frozenlist==1.3.1 138 | # via 139 | # aiohttp 140 | # aiosignal 141 | # eth-brownie 142 | hexbytes==0.2.3 143 | # via 144 | # eip712 145 | # eth-account 146 | # eth-brownie 147 | # eth-event 148 | # eth-rlp 149 | # web3 150 | hypothesis==6.27.3 151 | # via eth-brownie 152 | identify==2.5.9 153 | # via pre-commit 154 | idna==3.4 155 | # via 156 | # eth-brownie 157 | # requests 158 | # yarl 159 | inflection==0.5.0 160 | # via 161 | # eth-brownie 162 | # mythx-models 163 | # pythx 164 | iniconfig==1.1.1 165 | # via 166 | # eth-brownie 167 | # pytest 168 | ipfshttpclient==0.8.0a2 169 | # via 170 | # eth-brownie 171 | # web3 172 | isort==5.10.1 173 | # via -r requirements.in 174 | jsonschema==3.2.0 175 | # via 176 | # eth-brownie 177 | # mythx-models 178 | # web3 179 | lazy-object-proxy==1.7.1 180 | # via eth-brownie 181 | lru-dict==1.1.8 182 | # via 183 | # eth-brownie 184 | # web3 185 | mccabe==0.7.0 186 | # via flake8 187 | multiaddr==0.0.9 188 | # via 189 | # eth-brownie 190 | # ipfshttpclient 191 | multidict==6.0.2 192 | # via 193 | # aiohttp 194 | # eth-brownie 195 | # yarl 196 | mypy-extensions==0.4.3 197 | # via 198 | # black 199 | # eth-brownie 200 | mythx-models==1.9.1 201 | # via 202 | # eth-brownie 203 | # pythx 204 | netaddr==0.8.0 205 | # via 206 | # eth-brownie 207 | # multiaddr 208 | nodeenv==1.7.0 209 | # via pre-commit 210 | packaging==21.3 211 | # via 212 | # eth-brownie 213 | # pytest 214 | parsimonious==0.8.1 215 | # via 216 | # eth-abi 217 | # eth-brownie 218 | pathspec==0.10.1 219 | # via 220 | # black 221 | # eth-brownie 222 | platformdirs==2.5.2 223 | # via 224 | # black 225 | # eth-brownie 226 | # virtualenv 227 | pluggy==1.0.0 228 | # via 229 | # eth-brownie 230 | # pytest 231 | pre-commit==2.20.0 232 | # via -r requirements.in 233 | prompt-toolkit==3.0.31 234 | # via eth-brownie 235 | protobuf==3.19.5 236 | # via 237 | # eth-brownie 238 | # web3 239 | psutil==5.9.2 240 | # via eth-brownie 241 | py==1.11.0 242 | # via 243 | # eth-brownie 244 | # pytest 245 | # pytest-forked 246 | py-solc-ast==1.2.9 247 | # via eth-brownie 248 | py-solc-x==1.1.1 249 | # via eth-brownie 250 | pycodestyle==2.10.0 251 | # via flake8 252 | pycryptodome==3.15.0 253 | # via 254 | # eip712 255 | # eth-brownie 256 | # eth-hash 257 | # eth-keyfile 258 | # vyper 259 | pyflakes==3.0.1 260 | # via flake8 261 | pygments==2.13.0 262 | # via 263 | # eth-brownie 264 | # pygments-lexer-solidity 265 | pygments-lexer-solidity==0.7.0 266 | # via eth-brownie 267 | pyjwt==1.7.1 268 | # via 269 | # eth-brownie 270 | # pythx 271 | pyparsing==3.0.9 272 | # via 273 | # eth-brownie 274 | # packaging 275 | pyrsistent==0.18.1 276 | # via 277 | # eth-brownie 278 | # jsonschema 279 | pytest==6.2.5 280 | # via 281 | # eth-brownie 282 | # pytest-forked 283 | # pytest-xdist 284 | pytest-forked==1.4.0 285 | # via 286 | # eth-brownie 287 | # pytest-xdist 288 | pytest-xdist==1.34.0 289 | # via eth-brownie 290 | python-dateutil==2.8.1 291 | # via 292 | # eth-brownie 293 | # mythx-models 294 | # pythx 295 | python-dotenv==0.16.0 296 | # via eth-brownie 297 | pythx==1.6.1 298 | # via eth-brownie 299 | pyyaml==5.4.1 300 | # via 301 | # eth-brownie 302 | # pre-commit 303 | requests==2.28.1 304 | # via 305 | # eth-brownie 306 | # ipfshttpclient 307 | # py-solc-x 308 | # pythx 309 | # vvm 310 | # web3 311 | rlp==2.0.1 312 | # via 313 | # eth-account 314 | # eth-brownie 315 | # eth-rlp 316 | semantic-version==2.10.0 317 | # via 318 | # eth-brownie 319 | # py-solc-x 320 | # vvm 321 | # vyper 322 | six==1.16.0 323 | # via 324 | # asttokens 325 | # eth-brownie 326 | # jsonschema 327 | # multiaddr 328 | # parsimonious 329 | # pytest-xdist 330 | # python-dateutil 331 | sortedcontainers==2.4.0 332 | # via 333 | # eth-brownie 334 | # hypothesis 335 | toml==0.10.2 336 | # via 337 | # eth-brownie 338 | # pre-commit 339 | # pytest 340 | tomli==2.0.1 341 | # via 342 | # black 343 | # eth-brownie 344 | toolz==0.12.0 345 | # via 346 | # cytoolz 347 | # eth-brownie 348 | tqdm==4.64.1 349 | # via eth-brownie 350 | typing-extensions==4.4.0 351 | # via black 352 | urllib3==1.26.12 353 | # via 354 | # eth-brownie 355 | # requests 356 | varint==1.0.2 357 | # via 358 | # eth-brownie 359 | # multiaddr 360 | virtualenv==20.17.1 361 | # via pre-commit 362 | vvm==0.1.0 363 | # via eth-brownie 364 | vyper==0.3.7 365 | # via eth-brownie 366 | wcwidth==0.2.5 367 | # via 368 | # eth-brownie 369 | # prompt-toolkit 370 | web3==5.31.1 371 | # via eth-brownie 372 | websockets==9.1 373 | # via 374 | # eth-brownie 375 | # web3 376 | wheel==0.37.1 377 | # via 378 | # eth-brownie 379 | # vyper 380 | wrapt==1.14.1 381 | # via eth-brownie 382 | yarl==1.8.1 383 | # via 384 | # aiohttp 385 | # eth-brownie 386 | 387 | # The following packages are considered to be unsafe in a requirements file: 388 | # setuptools 389 | -------------------------------------------------------------------------------- /scripts/add_pools.py: -------------------------------------------------------------------------------- 1 | from brownie import Contract, Registry, accounts 2 | from brownie.exceptions import VirtualMachineError 3 | from brownie.network.gas.strategies import GasNowScalingStrategy 4 | 5 | from scripts.get_pool_data import get_pool_data 6 | from scripts.utils import pack_values 7 | 8 | # modify this prior to mainnet use 9 | DEPLOYER = "0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a" 10 | 11 | REGISTRY = "0x7D86446dDb609eD0F5f8684AcF30380a356b2B4c" 12 | GAUGE_CONTROLLER = "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB" 13 | 14 | RATE_METHOD_IDS = { 15 | "ATokenMock": "0x00000000", 16 | "aETH": "0x71ca337d", # ratio - requires a rate calculator deployment 17 | "cERC20": "0x182df0f5", # exchangeRateStored 18 | "IdleToken": "0x7ff9b596", # tokenPrice 19 | "renERC20": "0xbd6d894d", # exchangeRateCurrent 20 | "yERC20": "0x77c7b8fc", # getPricePerFullShare 21 | } 22 | 23 | gas_strategy = GasNowScalingStrategy("standard", "fast") 24 | 25 | 26 | def add_pool(data, registry, deployer, pool_name): 27 | swap = Contract(data["swap_address"]) 28 | token = data["lp_token_address"] 29 | n_coins = len(data["coins"]) 30 | decimals = pack_values([i.get("decimals", i.get("wrapped_decimals")) for i in data["coins"]]) 31 | 32 | if "base_pool" in data: 33 | # adding a metapool 34 | registry.add_metapool( 35 | swap, n_coins, token, decimals, pool_name, {"from": deployer, "gas_price": gas_strategy} 36 | ) 37 | return 38 | 39 | is_v1 = data["lp_contract"] == "CurveTokenV1" 40 | has_initial_A = hasattr(swap, "intitial_A") 41 | rate_info = "0x00000000" 42 | if "wrapped_contract" in data: 43 | rate_info = RATE_METHOD_IDS[data["wrapped_contract"]] 44 | if "rate_calculator_address" in data: 45 | # 24-bytes = 20-byte address + 4-byte fn sig 46 | rate_info = data["rate_calculator_address"] + rate_info[2:] 47 | 48 | if hasattr(swap, "exchange_underlying"): 49 | wrapped_decimals = pack_values( 50 | [i.get("wrapped_decimals", i["decimals"]) for i in data["coins"]] 51 | ) 52 | registry.add_pool( 53 | swap, 54 | n_coins, 55 | token, 56 | rate_info, 57 | wrapped_decimals, 58 | decimals, 59 | has_initial_A, 60 | is_v1, 61 | pool_name, 62 | {"from": deployer, "gas_price": gas_strategy}, 63 | ) 64 | else: 65 | use_lending_rates = pack_values(["wrapped_decimals" in i for i in data["coins"]]) 66 | registry.add_pool_without_underlying( 67 | swap, 68 | n_coins, 69 | token, 70 | rate_info, 71 | decimals, 72 | use_lending_rates, 73 | has_initial_A, 74 | is_v1, 75 | pool_name, 76 | {"from": deployer, "gas_price": gas_strategy}, 77 | ) 78 | 79 | 80 | def add_gauges(data, registry, deployer): 81 | pool = data["swap_address"] 82 | gauges = data["gauge_addresses"] 83 | gauges += ["0x0000000000000000000000000000000000000000"] * (10 - len(gauges)) 84 | 85 | if registry.get_gauges(pool)[0] != gauges: 86 | registry.set_liquidity_gauges(pool, gauges, {"from": deployer, "gas_price": gas_strategy}) 87 | 88 | 89 | def main(registry=REGISTRY, deployer=DEPLOYER): 90 | """ 91 | * Fetch pool data from Github 92 | * Add new pools to the existing registry deployment 93 | * Add / update pool gauges within the registry 94 | """ 95 | deployer = accounts.at(deployer, force=True) 96 | balance = deployer.balance() 97 | registry = Registry.at(registry) 98 | # sort keys leaving metapools last 99 | pool_data = sorted(get_pool_data().items(), key=lambda item: item[1].get("base_pool", "")) 100 | 101 | print("Adding pools to registry...") 102 | 103 | for name, data in pool_data: 104 | pool = data["swap_address"] 105 | if registry.get_n_coins(pool)[0] == 0: 106 | print(f"\nAdding {name}...") 107 | add_pool(data, registry, deployer, name) 108 | else: 109 | print(f"\n{name} has already been added to registry") 110 | 111 | gauges = data["gauge_addresses"] 112 | gauges = gauges + ["0x0000000000000000000000000000000000000000"] * (10 - len(gauges)) 113 | 114 | if registry.get_gauges(pool)[0] == gauges: 115 | print(f"{name} gauges are up-to-date") 116 | continue 117 | 118 | print(f"Updating gauges for {name}...") 119 | for gauge in data["gauge_addresses"]: 120 | try: 121 | Contract(GAUGE_CONTROLLER).gauge_types(gauge) 122 | except (ValueError, VirtualMachineError): 123 | print(f"Gauge {gauge} is not known to GaugeController, cannot add to registry") 124 | gauges = False 125 | break 126 | 127 | if gauges: 128 | registry.set_liquidity_gauges( 129 | pool, gauges, {"from": deployer, "gas_price": gas_strategy} 130 | ) 131 | 132 | print(f"Total gas used: {(balance - deployer.balance()) / 1e18:.4f} eth") 133 | -------------------------------------------------------------------------------- /scripts/deploy.py: -------------------------------------------------------------------------------- 1 | from brownie import ZERO_ADDRESS, AddressProvider, PoolInfo, Registry, Swaps, accounts 2 | from brownie.network.gas.strategies import GasNowScalingStrategy 3 | 4 | from scripts.add_pools import main as add_pools 5 | 6 | # modify this prior to mainnet use 7 | deployer = accounts.at("0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a", force=True) 8 | 9 | ADDRESS_PROVIDER = "0x0000000022D53366457F9d5E68Ec105046FC4383" 10 | GAUGE_CONTROLLER = "0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB" 11 | 12 | gas_strategy = GasNowScalingStrategy("standard", "fast") 13 | 14 | 15 | def deploy_registry(): 16 | """ 17 | Deploy `Registry`, add all current pools, and set the address in `AddressProvider`. 18 | """ 19 | balance = deployer.balance() 20 | 21 | provider = AddressProvider.at(ADDRESS_PROVIDER) 22 | registry = Registry.deploy( 23 | ADDRESS_PROVIDER, GAUGE_CONTROLLER, {"from": deployer, "gas_price": gas_strategy} 24 | ) 25 | add_pools(registry, deployer) 26 | provider.set_address(0, registry, {"from": deployer, "gas_price": gas_strategy}) 27 | 28 | print(f"Registry deployed to: {registry.address}") 29 | print(f"Total gas used: {(balance - deployer.balance()) / 1e18:.4f} eth") 30 | 31 | 32 | def deploy_pool_info(): 33 | """ 34 | Deploy `PoolInfo` and set the address in `AddressProvider`. 35 | """ 36 | balance = deployer.balance() 37 | 38 | provider = AddressProvider.at(ADDRESS_PROVIDER) 39 | 40 | pool_info = PoolInfo.deploy(provider, {"from": deployer, "gas_price": gas_strategy}) 41 | 42 | if provider.max_id() == 0: 43 | provider.add_new_id( 44 | pool_info, "PoolInfo Getters", {"from": deployer, "gas_price": gas_strategy} 45 | ) 46 | else: 47 | provider.set_address(1, pool_info, {"from": deployer, "gas_price": gas_strategy}) 48 | 49 | print(f"PoolInfo deployed to: {pool_info.address}") 50 | print(f"Total gas used: {(balance - deployer.balance()) / 1e18:.4f} eth") 51 | 52 | 53 | def deploy_swaps(): 54 | """ 55 | Deploy `Swaps` and set the address in `AddressProvider`. 56 | """ 57 | balance = deployer.balance() 58 | 59 | provider = AddressProvider.at(ADDRESS_PROVIDER) 60 | 61 | swaps = Swaps.deploy(provider, ZERO_ADDRESS, {"from": deployer, "gas_price": gas_strategy}) 62 | 63 | if provider.max_id() == 1: 64 | provider.add_new_id(swaps, "Exchanges", {"from": deployer, "gas_price": gas_strategy}) 65 | else: 66 | provider.set_address(2, swaps, {"from": deployer, "gas_price": gas_strategy}) 67 | 68 | print(f"PoolInfo deployed to: {swaps.address}") 69 | print(f"Total gas used: {(balance - deployer.balance()) / 1e18:.4f} eth") 70 | -------------------------------------------------------------------------------- /scripts/get_pool_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import requests 5 | 6 | GITHUB_POOLS = "https://api.github.com/repos/curvefi/curve-contract/contents/contracts/pools" 7 | GITHUB_POOLDATA = "https://raw.githubusercontent.com/curvefi/curve-contract/master/contracts/pools/{}/pooldata.json" # noqa: E501 8 | 9 | 10 | def get_pool_data(force_fetch: bool = False) -> dict: 11 | """ 12 | Fetch data about existing Curve pools from Github. 13 | 14 | Pool Data is pulled from `curve-contract/contacts/pools/[POOL_NAME]/pooldata.json` 15 | and stored at `./pooldata.json`. This JSON is then used for adding new pools to the registry 16 | and for forked-mainnet testing. 17 | 18 | To update the pools, delete `pooldata.json` or use `brownie run get_pool_data` 19 | """ 20 | path = Path(__file__).parent.parent.joinpath("pooldata.json") 21 | 22 | if not force_fetch and path.exists(): 23 | try: 24 | with path.open() as fp: 25 | return json.load(fp) 26 | except (json.JSONDecodeError, FileNotFoundError): 27 | pass 28 | 29 | print("Querying Github for pool deployments...") 30 | pool_data = {} 31 | pool_names = [i["name"] for i in requests.get(GITHUB_POOLS).json() if i["type"] == "dir"] 32 | 33 | for name in pool_names: 34 | data = requests.get(GITHUB_POOLDATA.format(name)).json() 35 | if "swap_address" not in data: 36 | print(f"Cannot add {name} - no deployment address!") 37 | continue 38 | pool_data[name] = data 39 | 40 | with path.open("w") as fp: 41 | json.dump(pool_data, fp, sort_keys=True, indent=2) 42 | 43 | print(f"Pool deployment data saved at {path.as_posix()}") 44 | return pool_data 45 | 46 | 47 | def main(): 48 | return get_pool_data(True) 49 | -------------------------------------------------------------------------------- /scripts/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | def pack_values(values: List[int]) -> bytes: 5 | """ 6 | Tightly pack integer values. 7 | 8 | Each number is represented as a single byte within a low-endian bytestring. The 9 | bytestring is then converted back to a `uint256` to be used in `Registry.add_pool` 10 | 11 | Arguments 12 | --------- 13 | values : list 14 | List of integer values to pack 15 | 16 | Returns 17 | ------- 18 | int 19 | 32 byte little-endian bytestring of packed values, converted to an integer 20 | """ 21 | assert max(values) < 256 22 | 23 | return sum(i << c * 8 for c, i in enumerate(values)) 24 | -------------------------------------------------------------------------------- /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 | 5 | 6 | @pytest.hookimpl(hookwrapper=True) 7 | def pytest_collection(session): 8 | yield 9 | 10 | base_path = Path(session.startdir) 11 | paths = set(Path(i.fspath).relative_to(base_path).parts[1] for i in session.items) 12 | 13 | if "forked" in paths and "local" in paths: 14 | raise pytest.UsageError( 15 | "Cannot run local and forked tests at the same time. Use `brownie test tests/fork`" 16 | " or `brownie test tests/local`" 17 | ) 18 | 19 | 20 | @pytest.fixture(autouse=True) 21 | def isolation_setup(fn_isolation): 22 | pass 23 | 24 | 25 | # account helpers 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def alice(accounts): 30 | yield accounts[0] 31 | 32 | 33 | @pytest.fixture(scope="session") 34 | def bob(accounts): 35 | yield accounts[1] 36 | -------------------------------------------------------------------------------- /tests/forked/conftest.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import Contract 4 | from brownie_tokens import MintableForkToken 5 | 6 | from scripts.add_pools import add_pool 7 | from scripts.get_pool_data import get_pool_data 8 | 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | _pooldata = get_pool_data() 11 | 12 | 13 | def pytest_addoption(parser): 14 | parser.addoption("--pool", help="comma-separated list of pools to target") 15 | parser.addoption("--once", action="store_true", help="Only run each test once per pool") 16 | 17 | 18 | def pytest_configure(config): 19 | # add custom markers 20 | config.addinivalue_line("markers", "once: only run this test once (no parametrization)") 21 | config.addinivalue_line("markers", "params: test parametrization filters") 22 | config.addinivalue_line("markers", "meta: only run test against metapools") 23 | config.addinivalue_line("markers", "skip_meta: do not run test against metapools") 24 | config.addinivalue_line( 25 | "markers", 26 | "itercoins: parametrize a test with one or more ranges, " 27 | "equal to `n_coins` for the active pool", 28 | ) 29 | 30 | 31 | def pytest_generate_tests(metafunc): 32 | # apply initial parametrization of `itercoins` 33 | for marker in metafunc.definition.iter_markers(name="itercoins"): 34 | for item in marker.args: 35 | metafunc.parametrize(item, range(9)) 36 | 37 | 38 | def pytest_collection_modifyitems(config, items): 39 | 40 | target_pool = config.getoption("pool") 41 | if target_pool: 42 | target_pool = target_pool.split(",") 43 | seen = {} 44 | 45 | for item in items.copy(): 46 | pool_name = item.callspec.params["pool_name"] 47 | pool_data = _pooldata[pool_name] 48 | base_pool = pool_data.get("base_pool") 49 | if target_pool and pool_name not in target_pool: 50 | items.remove(item) 51 | continue 52 | 53 | if base_pool: 54 | if next(item.iter_markers(name="skip_meta"), None): 55 | items.remove(item) 56 | continue 57 | else: 58 | if next(item.iter_markers(name="meta"), None): 59 | items.remove(item) 60 | continue 61 | 62 | # remove excess `itercoins` parametrized tests 63 | marker = next(item.iter_markers(name="itercoins"), False) 64 | if marker: 65 | is_underlying = marker.kwargs.get("underlying") 66 | if is_underlying and base_pool: 67 | # for metacoins, underlying must include the base pool 68 | n_coins = len(pool_data["coins"]) + len(_pooldata[base_pool]["coins"]) - 1 69 | else: 70 | n_coins = len(pool_data["coins"]) 71 | 72 | # apply `max` kwarg 73 | limit = min(n_coins - 1, marker.kwargs.get("max", n_coins)) 74 | 75 | values = [item.callspec.params[i] for i in marker.args] 76 | if len(set(values)) < len(values) or max(values) > limit: 77 | items.remove(item) 78 | continue 79 | 80 | if not is_underlying and not base_pool: 81 | # skip if `itercoins` is not marked as underlying and pool has no wrapped coins 82 | if next( 83 | (True for i in values if "wrapped_decimals" not in pool_data["coins"][i]), False 84 | ): 85 | items.remove(item) 86 | continue 87 | 88 | # filter parametrized tests when `once` is active 89 | # this must be the last filter applied, or we might completely skip a test 90 | if config.getoption("once") or next(item.iter_markers("once"), None): 91 | path = item.fspath 92 | seen.setdefault(pool_name, {}).setdefault(path, set()) 93 | 94 | if item.obj in seen[pool_name][path]: 95 | items.remove(item) 96 | continue 97 | seen[pool_name][path].add(item.obj) 98 | 99 | # hacky magic to ensure the correct number of tests is shown in collection report 100 | config.pluginmanager.get_plugin("terminalreporter")._numcollected = len(items) 101 | 102 | 103 | def pytest_collection_finish(session): 104 | # default to forked mainnet 105 | if session.items: 106 | brownie.network.connect("mainnet-fork") 107 | 108 | 109 | @pytest.fixture(scope="session", params=_pooldata.keys()) 110 | def pool_name(request): 111 | return request.param 112 | 113 | 114 | @pytest.fixture(scope="session") 115 | def pool_data(pool_name): 116 | # main parametrization fixture, pulls pool data from `./pooldata.json` 117 | return _pooldata[pool_name] 118 | 119 | 120 | @pytest.fixture(scope="session") 121 | def base_pool_data(pool_data): 122 | if "base_pool" in pool_data: 123 | return _pooldata[pool_data["base_pool"]] 124 | else: 125 | return None 126 | 127 | 128 | @pytest.fixture(scope="module") 129 | def provider(AddressProvider, alice): 130 | yield AddressProvider.deploy(alice, {"from": alice}) 131 | 132 | 133 | @pytest.fixture(scope="module") 134 | def registry(Registry, pool_name, pool_data, base_pool_data, alice, provider, gauge_controller): 135 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 136 | if base_pool_data: 137 | add_pool(base_pool_data, registry, alice, pool_data["base_pool"]) 138 | 139 | add_pool(pool_data, registry, alice, pool_name) 140 | 141 | yield registry 142 | 143 | 144 | @pytest.fixture(scope="module") 145 | def registry_swap(Swaps, alice, registry, provider, calculator): 146 | yield Swaps.deploy(provider, calculator, {"from": alice}) 147 | 148 | 149 | @pytest.fixture(scope="module") 150 | def calculator(CurveCalc, alice): 151 | yield CurveCalc.deploy({"from": alice}) 152 | 153 | 154 | @pytest.fixture(scope="module") 155 | def swap(pool_data): 156 | yield Contract(pool_data["swap_address"]) 157 | 158 | 159 | @pytest.fixture(scope="module") 160 | def base_swap(base_pool_data): 161 | if base_pool_data: 162 | return Contract(base_pool_data["swap_address"]) 163 | else: 164 | return None 165 | 166 | 167 | @pytest.fixture(scope="module") 168 | def lp_token(pool_data): 169 | yield Contract(pool_data["lp_token_address"]) 170 | 171 | 172 | @pytest.fixture(scope="module") 173 | def n_coins(pool_data): 174 | yield len(pool_data["coins"]) 175 | 176 | 177 | @pytest.fixture(scope="module") 178 | def gauge_controller(): 179 | yield Contract("0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB") 180 | 181 | 182 | class _MintableTestToken(MintableForkToken): 183 | 184 | _rate_methods = ( 185 | "exchangeRateStored", 186 | "exchangeRateCurrent", 187 | "getPricePerFullShare", 188 | "get_virtual_price", 189 | ) 190 | 191 | def __init__(self, coin_data, is_wrapped): 192 | self._coin_data = coin_data 193 | 194 | wrapped_address = coin_data.get("wrapped_address") 195 | underlying_address = coin_data.get("underlying_address") 196 | if is_wrapped: 197 | address = wrapped_address or underlying_address 198 | else: 199 | address = underlying_address or wrapped_address 200 | super().__init__(address) 201 | 202 | if is_wrapped and wrapped_address: 203 | self._rate_fn = next(getattr(self, i) for i in self._rate_methods if hasattr(self, i)) 204 | else: 205 | if "base_pool_token" in coin_data: 206 | base_pool = next( 207 | i for i in _pooldata.values() if i.get("lp_token_address") == self.address 208 | ) 209 | self._rate_fn = Contract(base_pool["swap_address"]).get_virtual_price 210 | else: 211 | self._rate_fn = None 212 | 213 | def _get_rate(self): 214 | if not self._rate_fn: 215 | return 10**18 216 | return self._rate_fn.call() 217 | 218 | 219 | @pytest.fixture(scope="module") 220 | def wrapped_coins(pool_data): 221 | yield [_MintableTestToken(i, True) for i in pool_data["coins"]] 222 | 223 | 224 | @pytest.fixture(scope="module") 225 | def underlying_coins(pool_data, base_pool_data): 226 | if base_pool_data: 227 | coins = [_MintableTestToken(i, False) for i in pool_data["coins"][:-1]] 228 | coins += [_MintableTestToken(i, False) for i in base_pool_data["coins"]] 229 | yield coins 230 | else: 231 | yield [_MintableTestToken(i, False) for i in pool_data["coins"]] 232 | 233 | 234 | @pytest.fixture(scope="module") 235 | def underlying_decimals(pool_data, base_pool_data): 236 | # number of decimal places for each underlying coin in the active pool 237 | decimals = [i.get("decimals", i.get("wrapped_decimals")) for i in pool_data["coins"]] 238 | 239 | if base_pool_data is None: 240 | return decimals 241 | base_decimals = [i.get("decimals", i.get("wrapped_decimals")) for i in base_pool_data["coins"]] 242 | return decimals[:-1] + base_decimals 243 | 244 | 245 | @pytest.fixture(scope="module") 246 | def wrapped_decimals(pool_data): 247 | # number of decimal places for each wrapped coin in the active pool 248 | yield [i.get("wrapped_decimals", i.get("decimals")) for i in pool_data["coins"]] 249 | -------------------------------------------------------------------------------- /tests/forked/test_exchange_amounts.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie.test import given, strategy 3 | from hypothesis import Phase, settings 4 | 5 | 6 | @pytest.mark.itercoins("send", "recv") 7 | @given(st_amounts=strategy("decimal[100]", min_value="0.05", max_value=10, places=2, unique=True)) 8 | @settings(max_examples=3, phases=[Phase.generate]) 9 | def test_get_amounts_wrapped( 10 | registry_swap, swap, wrapped_coins, wrapped_decimals, send, recv, st_amounts 11 | ): 12 | base_amount = 10 ** wrapped_decimals[send] 13 | st_amounts = [int(base_amount * i) for i in st_amounts] 14 | 15 | send = wrapped_coins[send] 16 | recv = wrapped_coins[recv] 17 | 18 | amounts = registry_swap.get_exchange_amounts(swap, send, recv, st_amounts) 19 | 20 | for i in range(100): 21 | # `get_exchange_amount` is a thin wrapper that calls `swap.get_dy` 22 | amount = registry_swap.get_exchange_amount(swap, send, recv, st_amounts[i]) 23 | assert abs(amount - amounts[i]) <= 1 24 | 25 | 26 | @pytest.mark.itercoins("send", "recv", underlying=True) 27 | @given(st_amounts=strategy("decimal[100]", min_value="0.05", max_value=10, places=2, unique=True)) 28 | @settings(max_examples=3, phases=[Phase.generate]) 29 | def test_get_amounts_underlying( 30 | registry_swap, swap, underlying_coins, underlying_decimals, send, recv, st_amounts 31 | ): 32 | base_amount = 10 ** underlying_decimals[send] 33 | st_amounts = [int(base_amount * i) for i in st_amounts] 34 | 35 | send = underlying_coins[send] 36 | recv = underlying_coins[recv] 37 | 38 | amounts = registry_swap.get_exchange_amounts(swap, send, recv, st_amounts) 39 | 40 | for i in range(100): 41 | # `get_exchange_amount` is a thin wrapper that calls `swap.get_dy_underlying` 42 | amount = registry_swap.get_exchange_amount(swap, send, recv, st_amounts[i]) 43 | assert abs(amount - amounts[i]) <= 1 44 | -------------------------------------------------------------------------------- /tests/forked/test_exchange_best_rate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS, ZERO_ADDRESS 3 | 4 | 5 | @pytest.fixture(scope="module") 6 | def provider(AddressProvider): 7 | yield AddressProvider.at("0x0000000022D53366457F9d5E68Ec105046FC4383") 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def registry_swap(Swaps, alice, provider, calculator, underlying_coins, wrapped_coins): 12 | yield Swaps.deploy(provider, ZERO_ADDRESS, {"from": alice}) 13 | 14 | 15 | @pytest.mark.itercoins("send", "recv", underlying=True) 16 | def test_exchange( 17 | alice, bob, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 18 | ): 19 | amount = 10 ** underlying_decimals[send] 20 | 21 | send = underlying_coins[send] 22 | recv = underlying_coins[recv] 23 | balance = bob.balance() 24 | value = 10**18 if send == ETH_ADDRESS else 0 25 | if send != ETH_ADDRESS: 26 | send.approve(registry_swap, 2**256 - 1, {"from": alice}) 27 | send._mint_for_testing(alice, amount, {"from": alice}) 28 | 29 | registry_swap.exchange_with_best_rate( 30 | send, recv, amount, 0, bob, {"from": alice, "value": value} 31 | ) 32 | 33 | # we don't verify the amounts here, just that the expected tokens were exchanged 34 | if send == ETH_ADDRESS: 35 | assert alice.balance() < balance 36 | else: 37 | assert send.balanceOf(alice) == 0 38 | 39 | if recv == ETH_ADDRESS: 40 | assert bob.balance() > balance 41 | else: 42 | assert recv.balanceOf(bob) > 0 43 | 44 | 45 | @pytest.mark.itercoins("send", "recv") 46 | def test_exchange_wrapped(alice, bob, registry_swap, wrapped_coins, wrapped_decimals, send, recv): 47 | amount = 10 ** wrapped_decimals[send] 48 | 49 | send = wrapped_coins[send] 50 | recv = wrapped_coins[recv] 51 | balance = alice.balance() 52 | value = 10**18 if send == ETH_ADDRESS else 0 53 | if send != ETH_ADDRESS: 54 | send.approve(registry_swap, 2**256 - 1, {"from": alice}) 55 | send._mint_for_testing(alice, amount, {"from": alice}) 56 | 57 | registry_swap.exchange_with_best_rate( 58 | send, recv, amount, 0, bob, {"from": alice, "value": value} 59 | ) 60 | 61 | if send == ETH_ADDRESS: 62 | assert alice.balance() < balance 63 | else: 64 | assert send.balanceOf(alice) == 0 65 | 66 | if recv == ETH_ADDRESS: 67 | assert bob.balance() > balance 68 | else: 69 | assert recv.balanceOf(bob) > 0 70 | -------------------------------------------------------------------------------- /tests/forked/test_exchanges.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | 7 | @pytest.fixture(scope="module", autouse=True) 8 | def initial_approvals(registry_swap, alice, underlying_coins): 9 | for coin in underlying_coins: 10 | if coin != "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 11 | coin.approve(registry_swap, 2**256 - 1, {"from": alice}) 12 | 13 | 14 | @pytest.mark.itercoins("send", "recv", underlying=True) 15 | def test_exchange(alice, registry_swap, swap, underlying_coins, underlying_decimals, send, recv): 16 | amount = 10 ** underlying_decimals[send] 17 | 18 | send = underlying_coins[send] 19 | recv = underlying_coins[recv] 20 | balance = alice.balance() 21 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 22 | value = 10**18 23 | else: 24 | send._mint_for_testing(alice, amount, {"from": alice}) 25 | value = 0 26 | 27 | registry_swap.exchange(swap, send, recv, amount, 0, {"from": alice, "value": value}) 28 | 29 | # we don't verify the amounts here, just that the expected tokens were exchanged 30 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 31 | assert alice.balance() < balance 32 | else: 33 | assert send.balanceOf(alice) == 0 34 | 35 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 36 | assert alice.balance() > balance 37 | else: 38 | assert recv.balanceOf(alice) > 0 39 | 40 | 41 | @pytest.mark.itercoins("send", "recv") 42 | def test_exchange_wrapped(alice, registry_swap, swap, wrapped_coins, wrapped_decimals, send, recv): 43 | amount = 10 ** wrapped_decimals[send] 44 | 45 | send = wrapped_coins[send] 46 | recv = wrapped_coins[recv] 47 | balance = alice.balance() 48 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 49 | value = 10**18 50 | else: 51 | send._mint_for_testing(alice, amount, {"from": alice}) 52 | value = 0 53 | 54 | registry_swap.exchange(swap, send, recv, amount, 0, {"from": alice, "value": value}) 55 | 56 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 57 | assert alice.balance() < balance 58 | else: 59 | assert send.balanceOf(alice) == 0 60 | 61 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 62 | assert alice.balance() > balance 63 | else: 64 | assert recv.balanceOf(alice) > 0 65 | 66 | 67 | @pytest.mark.itercoins("send", "recv", underlying=True) 68 | def test_min_expected( 69 | alice, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 70 | ): 71 | # exchange should revert when `expected` is higher than received amount 72 | amount = 10 ** underlying_decimals[send] 73 | send = underlying_coins[send] 74 | recv = underlying_coins[recv] 75 | 76 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 77 | value = 10**18 78 | else: 79 | send._mint_for_testing(alice, amount, {"from": alice}) 80 | value = 0 81 | 82 | expected = registry_swap.get_exchange_amount(swap, send, recv, amount) 83 | with brownie.reverts(): 84 | registry_swap.exchange( 85 | swap, send, recv, amount, expected + 1, {"from": alice, "value": value} 86 | ) 87 | -------------------------------------------------------------------------------- /tests/forked/test_getters.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | 7 | def test_get_coins(registry, swap, n_coins): 8 | coins = registry.get_coins(swap) 9 | for i in range(n_coins): 10 | assert coins[i] == swap.coins(i) 11 | 12 | assert coins[n_coins] == ZERO_ADDRESS 13 | with brownie.reverts(): 14 | swap.coins(n_coins) 15 | 16 | 17 | def test_get_decimals(Contract, registry, swap, wrapped_coins): 18 | decimals = registry.get_decimals(swap) 19 | for i, coin in enumerate(wrapped_coins): 20 | assert coin.decimals() == decimals[i] 21 | 22 | 23 | def test_get_underlying_decimals(Contract, registry, swap, underlying_coins, pool_data): 24 | decimals = registry.get_underlying_decimals(swap) 25 | for i, coin in enumerate(underlying_coins): 26 | if "decimals" in pool_data["coins"][i]: 27 | assert coin.decimals() == decimals[i] 28 | else: 29 | assert decimals[i] == 0 30 | 31 | 32 | def test_get_virtual_price_from_lp_token(registry, swap, lp_token): 33 | assert registry.get_virtual_price_from_lp_token(lp_token) == swap.get_virtual_price() 34 | 35 | 36 | def test_get_rates(registry, swap, wrapped_coins): 37 | rates = registry.get_rates(swap) 38 | for i, coin in enumerate(wrapped_coins): 39 | assert rates[i] == coin._get_rate() 40 | 41 | 42 | def test_get_balances(registry, swap, n_coins): 43 | balances = registry.get_balances(swap) 44 | for i in range(n_coins): 45 | assert swap.balances(i) == balances[i] 46 | 47 | 48 | @pytest.mark.skip_meta 49 | def test_get_underlying_balances(registry, swap, n_coins, wrapped_coins, underlying_coins): 50 | underlying_balances = registry.get_underlying_balances(swap) 51 | 52 | for i in range(n_coins): 53 | balance = swap.balances(i) 54 | if wrapped_coins[i] == underlying_coins[i]: 55 | assert balance == underlying_balances[i] 56 | else: 57 | rate = wrapped_coins[i]._get_rate() 58 | decimals = underlying_coins[i].decimals() 59 | assert balance * rate // 10**decimals == underlying_balances[i] 60 | 61 | 62 | @pytest.mark.meta 63 | def test_get_underlying_balances_meta( 64 | bob, registry, swap, base_swap, wrapped_coins, underlying_coins 65 | ): 66 | underlying_balances = registry.get_underlying_balances(swap) 67 | 68 | for i in range(len(wrapped_coins) - 1): 69 | assert underlying_balances[i] == swap.balances(i) 70 | 71 | idx = len(wrapped_coins) - 1 72 | base_n_coins = len(underlying_coins) - 1 73 | lp_token = wrapped_coins[idx] 74 | lp_balance = swap.balances(idx) 75 | 76 | # some voodoo here - we transfer the balance of the base LP tokens from `swap` to `bob` 77 | # and then withdraw them from `base_pool` to get the actual underlying amounts 78 | lp_token.transfer(bob, lp_balance, {"from": swap}) 79 | base_swap.remove_liquidity(lp_balance, [0] * base_n_coins, {"from": bob}) 80 | 81 | for i, coin in enumerate(underlying_coins[1:], start=1): 82 | assert coin.balanceOf(bob) == underlying_balances[i] 83 | -------------------------------------------------------------------------------- /tests/forked/test_input_amount.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie.test import given, strategy 3 | from hypothesis import Phase, settings 4 | 5 | 6 | @pytest.mark.itercoins("send", "recv", underlying=True) 7 | @given(st_amount=strategy("decimal", min_value="0.5", max_value=1000, places=2)) 8 | @settings(max_examples=50, phases=[Phase.generate]) 9 | def test_get_input_amount( 10 | registry_swap, swap, underlying_coins, underlying_decimals, send, recv, st_amount 11 | ): 12 | dy = int(10 ** underlying_decimals[recv] * st_amount) 13 | 14 | send = underlying_coins[send] 15 | recv = underlying_coins[recv] 16 | 17 | dx = registry_swap.get_input_amount(swap, send, recv, dy) 18 | amount = registry_swap.get_exchange_amount(swap, send, recv, dx) 19 | assert abs(amount - dy) <= 1 20 | 21 | 22 | @pytest.mark.itercoins("send", "recv") 23 | @given(st_amount=strategy("decimal", min_value="0.5", max_value=1000, places=2)) 24 | @settings(max_examples=50, phases=[Phase.generate]) 25 | def test_get_input_amount_wrapped( 26 | registry_swap, swap, wrapped_coins, wrapped_decimals, send, recv, st_amount 27 | ): 28 | dy = int(10 ** wrapped_decimals[recv] * st_amount) 29 | 30 | send = wrapped_coins[send] 31 | recv = wrapped_coins[recv] 32 | 33 | dx = registry_swap.get_input_amount(swap, send, recv, dy) 34 | amount = registry_swap.get_exchange_amount(swap, send, recv, dx) 35 | assert abs(amount - dy) <= 1 36 | -------------------------------------------------------------------------------- /tests/local/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ERC20, ERC20NoReturn, ERC20ReturnFalse, cERC20, yERC20 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | RATE_METHOD_IDS = { 7 | "cERC20": cERC20.signatures["exchangeRateStored"], 8 | "yERC20": yERC20.signatures["getPricePerFullShare"], 9 | } 10 | 11 | 12 | # setup 13 | 14 | 15 | def pytest_addoption(parser): 16 | parser.addoption( 17 | "--once", action="store_true", help="Only run each test once (no parametrization)" 18 | ) 19 | 20 | 21 | def pytest_configure(config): 22 | # add custom markers 23 | config.addinivalue_line("markers", "once: only run this test once (no parametrization)") 24 | config.addinivalue_line("markers", "params: test parametrization filters") 25 | config.addinivalue_line( 26 | "markers", 27 | "itercoins: parametrize a test with one or more ranges, " 28 | "equal to `n_coins` for the active pool", 29 | ) 30 | config.addinivalue_line( 31 | "markers", 32 | "itermetacoins: parametrize a test with one or more ranges, " 33 | "equal to `n_metacoins` for the active pool", 34 | ) 35 | 36 | 37 | def pytest_generate_tests(metafunc): 38 | # apply initial parametrization of `itercoins` 39 | for marker in metafunc.definition.iter_markers(name="itercoins"): 40 | for item in marker.args: 41 | metafunc.parametrize(item, range(9)) 42 | 43 | # apply initial parametrization of `itermetacoins` 44 | for marker in metafunc.definition.iter_markers(name="itermetacoins"): 45 | for item in marker.args: 46 | metafunc.parametrize(item, range(9)) 47 | 48 | 49 | def pytest_collection_modifyitems(config, items): 50 | seen = {} 51 | for item in items.copy(): 52 | # remove excess `itercoins` parametrized tests 53 | marker = next(item.iter_markers(name="itercoins"), False) 54 | if marker: 55 | n_coins = item.callspec.params["n_coins"] 56 | 57 | limit = marker.kwargs.get("max", 9) 58 | values = [item.callspec.params[i] for i in marker.args] 59 | if max(values) >= n_coins or len(set(values)) < len(values) or max(values) > limit: 60 | items.remove(item) 61 | continue 62 | 63 | # remove excess `itermetacoins` parametrized tests 64 | marker = next(item.iter_markers(name="itermetacoins"), False) 65 | if marker: 66 | n_coins = item.callspec.params["n_metacoins"] 67 | 68 | limit = marker.kwargs.get("max", 9) 69 | values = [item.callspec.params[i] for i in marker.args] 70 | if max(values) >= n_coins or len(set(values)) < len(values) or max(values) > limit: 71 | items.remove(item) 72 | continue 73 | 74 | # remove parametrized tests that do not match `params` marker kwargs 75 | marker = next(item.iter_markers(name="params"), False) 76 | if marker: 77 | params = item.callspec.params 78 | if next((k for k, v in marker.kwargs.items() if params.get(k) != v), None): 79 | items.remove(item) 80 | continue 81 | 82 | # filter parametrized tests when `once` is active 83 | # this must be the last filter applied, or we might completely skip a test 84 | if config.getoption("once") or next(item.iter_markers("once"), None): 85 | path = item.fspath 86 | seen.setdefault(path, set()) 87 | if item.obj in seen[path]: 88 | items.remove(item) 89 | continue 90 | seen[path].add(item.obj) 91 | 92 | # hacky magic to ensure the correct number of tests is shown in collection report 93 | config.pluginmanager.get_plugin("terminalreporter")._numcollected = len(items) 94 | 95 | 96 | @pytest.fixture(autouse=True) 97 | def isolation_setup(fn_isolation): 98 | pass 99 | 100 | 101 | # simple deployment fixtures (no parametrization) 102 | 103 | 104 | @pytest.fixture(scope="module") 105 | def gauge_controller(GaugeControllerMock, alice): 106 | yield GaugeControllerMock.deploy({"from": alice}) 107 | 108 | 109 | @pytest.fixture(scope="module") 110 | def calculator(CurveCalc, alice): 111 | yield CurveCalc.deploy({"from": alice}) 112 | 113 | 114 | @pytest.fixture(scope="module") 115 | def provider(AddressProvider, alice): 116 | yield AddressProvider.deploy(alice, {"from": alice}) 117 | 118 | 119 | @pytest.fixture(scope="module") 120 | def registry(Registry, alice, provider, gauge_controller): 121 | contract = Registry.deploy(provider, gauge_controller, {"from": alice}) 122 | provider.set_address(0, contract, {"from": alice}) 123 | yield contract 124 | 125 | 126 | @pytest.fixture(scope="module") 127 | def registry_pool_info(PoolInfo, alice, provider): 128 | yield PoolInfo.deploy(provider, {"from": alice}) 129 | 130 | 131 | @pytest.fixture(scope="module") 132 | def registry_swap(Swaps, alice, provider, registry, calculator): 133 | provider.set_address(0, registry, {"from": alice}) 134 | yield Swaps.deploy(provider, calculator, {"from": alice}) 135 | 136 | 137 | @pytest.fixture(scope="module") 138 | def lp_token(alice): 139 | return ERC20.deploy("MetaTest Token", "MTST", 18, {"from": alice}) 140 | 141 | 142 | @pytest.fixture(scope="module") 143 | def meta_lp_token(alice): 144 | return ERC20.deploy("MetaTest Token", "MTST", 18, {"from": alice}) 145 | 146 | 147 | # private deployments fixtures 148 | # deploying prior to parametrization of the public fixtures avoids excessive deployments 149 | 150 | 151 | @pytest.fixture(scope="module") 152 | def _underlying_decimals(): 153 | return [18, 8, 6, 18] 154 | 155 | 156 | @pytest.fixture(scope="module") 157 | def _wrapped_decimals(): 158 | return [8, 12, 16, 18] 159 | 160 | 161 | @pytest.fixture(scope="module") 162 | def _meta_decimals(): 163 | return [6, 18, 8] 164 | 165 | 166 | @pytest.fixture(scope="module") 167 | def _underlying_coins(_underlying_decimals, alice): 168 | deployers = [ERC20, ERC20NoReturn, ERC20ReturnFalse] 169 | coins = [] 170 | for i, (deployer, decimals) in enumerate(zip(deployers, _underlying_decimals)): 171 | contract = deployer.deploy(f"Test Token {i}", f"TST{i}", decimals, {"from": alice}) 172 | coins.append(contract) 173 | coins.append("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") 174 | 175 | return coins 176 | 177 | 178 | @pytest.fixture(scope="module") 179 | def _wrapped_coins(_underlying_coins, _wrapped_decimals, lending, alice): 180 | coins = [] 181 | for i, (coin, decimals) in enumerate(zip(_underlying_coins, _wrapped_decimals)): 182 | if coin == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 183 | coins.append(coin) 184 | continue 185 | contract = lending.deploy( 186 | f"Wrapped Test Token {i}", f"wTST{i}", decimals, coin, 10**18, {"from": alice} 187 | ) 188 | coins.append(contract) 189 | 190 | return coins 191 | 192 | 193 | @pytest.fixture(scope="module") 194 | def _meta_coins(_meta_decimals, alice): 195 | deployers = [ERC20, ERC20NoReturn, ERC20ReturnFalse] 196 | coins = [] 197 | for i, (deployer, decimals) in enumerate(zip(deployers, _meta_decimals)): 198 | contract = deployer.deploy(f"MetaTest Token {i}", f"MT{i}", decimals, {"from": alice}) 199 | coins.append(contract) 200 | 201 | return coins 202 | 203 | 204 | # parametrized fixtures 205 | 206 | 207 | @pytest.fixture(scope="module", params=(True, False), ids=("v1", "v2")) 208 | def is_v1(request): 209 | return request.param 210 | 211 | 212 | @pytest.fixture(scope="module", params=("yERC20", "cERC20"), ids=("yearn", "comp")) 213 | def lending(request): 214 | yield globals()[request.param] 215 | 216 | 217 | @pytest.fixture(scope="module", params=range(4, 1, -1), ids=(f"{i} coins" for i in range(4, 1, -1))) 218 | def n_coins(_underlying_coins, request): 219 | return request.param 220 | 221 | 222 | @pytest.fixture( 223 | scope="module", params=range(4, 1, -1), ids=(f"{i} metacoins" for i in range(4, 1, -1)) 224 | ) 225 | def n_metacoins(_meta_coins, request): 226 | return request.param 227 | 228 | 229 | @pytest.fixture(scope="module") 230 | def underlying_decimals(_underlying_decimals, n_coins): 231 | return _underlying_decimals[:n_coins] 232 | 233 | 234 | @pytest.fixture(scope="module") 235 | def wrapped_decimals(_wrapped_decimals, n_coins): 236 | return _wrapped_decimals[:n_coins] 237 | 238 | 239 | @pytest.fixture(scope="module") 240 | def meta_decimals(_meta_decimals, n_metacoins): 241 | return _meta_decimals[: n_metacoins - 1] + [18] 242 | 243 | 244 | @pytest.fixture(scope="module") 245 | def underlying_coins(alice, _underlying_coins, n_coins): 246 | return _underlying_coins[:n_coins] 247 | 248 | 249 | @pytest.fixture(scope="module") 250 | def wrapped_coins(_wrapped_coins, n_coins): 251 | return _wrapped_coins[:n_coins] 252 | 253 | 254 | @pytest.fixture(scope="module") 255 | def meta_coins(alice, _meta_coins, n_metacoins, lp_token): 256 | return _meta_coins[: n_metacoins - 1] + [lp_token] 257 | 258 | 259 | @pytest.fixture(scope="module") 260 | def rate_method_id(wrapped_coins): 261 | key = wrapped_coins[0]._name 262 | return RATE_METHOD_IDS[key] 263 | 264 | 265 | @pytest.fixture(scope="module") 266 | def swap(PoolMockV1, PoolMockV2, alice, underlying_coins, is_v1): 267 | 268 | if is_v1: 269 | deployer = PoolMockV1 270 | else: 271 | deployer = PoolMockV2 272 | 273 | n_coins = len(underlying_coins) 274 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins)) 275 | 276 | contract = deployer.deploy( 277 | n_coins, underlying_coins, [ZERO_ADDRESS] * 4, 70, 4000000, {"from": alice} 278 | ) 279 | return contract 280 | 281 | 282 | @pytest.fixture(scope="module") 283 | def lending_swap(PoolMockV1, PoolMockV2, alice, wrapped_coins, underlying_coins, is_v1): 284 | if is_v1: 285 | deployer = PoolMockV1 286 | else: 287 | deployer = PoolMockV2 288 | 289 | n_coins = len(underlying_coins) 290 | wrapped_coins = wrapped_coins + [ZERO_ADDRESS] * (4 - len(wrapped_coins)) 291 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins)) 292 | 293 | contract = deployer.deploy( 294 | n_coins, wrapped_coins, underlying_coins, 70, 4000000, {"from": alice} 295 | ) 296 | return contract 297 | 298 | 299 | @pytest.fixture(scope="module") 300 | def meta_swap(MetaPoolMock, alice, swap, meta_coins, underlying_coins, n_metacoins, n_coins): 301 | meta_coins = meta_coins + [ZERO_ADDRESS] * (4 - len(meta_coins)) 302 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - len(underlying_coins)) 303 | return MetaPoolMock.deploy( 304 | n_metacoins, n_coins, swap, meta_coins, underlying_coins, 70, 4000000, {"from": alice} 305 | ) 306 | 307 | 308 | @pytest.fixture(scope="module") 309 | def liquidity_gauge(LiquidityGaugeMock, alice, gauge_controller, lp_token): 310 | gauge = LiquidityGaugeMock.deploy(lp_token, {"from": alice}) 311 | gauge_controller._set_gauge_type(gauge, 1, {"from": alice}) 312 | yield gauge 313 | 314 | 315 | @pytest.fixture(scope="module") 316 | def liquidity_gauge_meta(LiquidityGaugeMock, alice, gauge_controller, meta_lp_token): 317 | gauge = LiquidityGaugeMock.deploy(meta_lp_token, {"from": alice}) 318 | gauge_controller._set_gauge_type(gauge, 2, {"from": alice}) 319 | yield gauge 320 | -------------------------------------------------------------------------------- /tests/local/integration/coin_register_utils.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | from collections import Counter, defaultdict 3 | from enum import IntEnum 4 | from typing import List, Tuple 5 | 6 | 7 | class PoolType(IntEnum): 8 | 9 | BASE = 0 10 | LENDING = 1 11 | META = 2 12 | 13 | 14 | class Pool: 15 | def __init__( 16 | self, 17 | address: str, 18 | coins: List[str], 19 | lp_token: str, 20 | underlying_coins: List[str] = None, 21 | base_coins: List[str] = None, 22 | pool_type: PoolType = PoolType.BASE, 23 | ): 24 | self.address = address 25 | self.coins = coins 26 | self.lp_token = lp_token 27 | self.underlying_coins = underlying_coins 28 | self.base_coins = base_coins 29 | self.pool_type = pool_type 30 | 31 | @classmethod 32 | def base_pool(cls, address: str, coins: List[str], lp_token: str): 33 | return cls(address, coins, lp_token) 34 | 35 | @classmethod 36 | def lending_pool( 37 | cls, address: str, coins: List[str], lp_token: str, underlying_coins: List[str] 38 | ): 39 | return cls(address, coins, lp_token, underlying_coins, pool_type=PoolType.LENDING) 40 | 41 | @classmethod 42 | def meta_pool(cls, address: str, coins: List[str], lp_token: str, base_coins: List[str]): 43 | return cls(address, coins, lp_token, base_coins=base_coins, pool_type=PoolType.META) 44 | 45 | 46 | class Registry: 47 | """A Registry instance state emulator.""" 48 | 49 | def __init__(self): 50 | # coin register count - if count < 0 then it doesn't exist 51 | self._coin_register_counter = Counter() 52 | # number of unique coins registered 53 | self.coin_count = 0 54 | # unique coins registered 55 | self.get_coin = set() 56 | # coin_a -> coin_b -> # of time registered 57 | self._coin_swap_register = defaultdict(Counter) 58 | # coin -> # of unique coins available to swap with 59 | self.get_coin_swap_count = Counter() 60 | # unique set of coins (coin_b) which can swap against coin_a 61 | self.get_coin_swap_complement = defaultdict(set) 62 | # timestamp of last update 63 | self.last_updated = 0 64 | 65 | # available pools with their type 66 | self.pools = [] 67 | 68 | @property 69 | def base_pools(self): 70 | return [pool for pool in self.pools if pool.pool_type == PoolType.BASE] 71 | 72 | @property 73 | def lending_pools(self): 74 | return [pool for pool in self.pools if pool.pool_type == PoolType.LENDING] 75 | 76 | @property 77 | def meta_pools(self): 78 | return [pool for pool in self.pools if pool.pool_type == PoolType.META] 79 | 80 | def _register_coins(self, coins: List[str]): 81 | self._coin_register_counter.update(coins) 82 | self.get_coin = set((+self._coin_register_counter).keys()) 83 | self.coin_count = len(self.get_coin) 84 | 85 | def _register_coin_pairs(self, pairings: List[Tuple[str, str]]): 86 | coins = set() 87 | for coin_a, coin_b in pairings: 88 | coins.update([coin_a, coin_b]) 89 | self._coin_swap_register[coin_a].update([coin_b]) 90 | self._coin_swap_register[coin_b].update([coin_a]) 91 | 92 | for coin in coins: 93 | self.get_coin_swap_complement[coin] = set((+self._coin_swap_register[coin]).keys()) 94 | self.get_coin_swap_count[coin] = len(self.get_coin_swap_complement[coin]) 95 | 96 | def _unregister_coins(self, coins: List[str]): 97 | self._coin_register_counter.subtract(coins) 98 | self.get_coin = set((+self._coin_register_counter).keys()) 99 | self.coin_count = len(self.get_coin) 100 | 101 | def _unregister_coin_pairs(self, pairings: List[Tuple[str, str]]): 102 | coins = set() 103 | for coin_a, coin_b in pairings: 104 | coins.update([coin_a, coin_b]) 105 | self._coin_swap_register[coin_a].subtract([coin_b]) 106 | self._coin_swap_register[coin_b].subtract([coin_a]) 107 | 108 | for coin in coins: 109 | self.get_coin_swap_complement[coin] = set((+self._coin_swap_register[coin]).keys()) 110 | self.get_coin_swap_count[coin] = len(self.get_coin_swap_complement[coin]) 111 | 112 | def add_pool( 113 | self, 114 | address: str, 115 | wrapped_coins: List[str], 116 | underlying_coins: List[str], 117 | lp_token: str, 118 | timestamp: int, 119 | ): 120 | """Add lending pool and update state.""" 121 | # append pool to list of pools 122 | lending_pool = Pool.lending_pool(address, wrapped_coins, lp_token, underlying_coins) 123 | self.pools.append(lending_pool) 124 | 125 | # register coins 126 | self._register_coins(it.chain(wrapped_coins, underlying_coins)) 127 | 128 | # register coin pairs 129 | wrapped_pairs = it.combinations(wrapped_coins, 2) 130 | underlying_pairs = it.combinations(underlying_coins, 2) 131 | self._register_coin_pairs(it.chain(wrapped_pairs, underlying_pairs)) 132 | 133 | # update timestamp 134 | self.last_updated = timestamp 135 | 136 | def add_pool_without_underlying( 137 | self, address: str, coins: List[str], lp_token: str, timestamp: int 138 | ): 139 | """Add base pool and update state.""" 140 | # append pool to list of pools 141 | base_pool = Pool.base_pool(address, coins, lp_token) 142 | self.pools.append(base_pool) 143 | 144 | # register coins 145 | self._register_coins(coins) 146 | 147 | # register coin pairs 148 | coin_pairs = it.combinations(coins, 2) 149 | self._register_coin_pairs(coin_pairs) 150 | 151 | # update timestamp 152 | self.last_updated = timestamp 153 | 154 | def add_metapool( 155 | self, 156 | address: str, 157 | meta_coins: List[str], 158 | lp_token: str, 159 | base_coins: List[str], 160 | timestamp: int, 161 | ): 162 | """Add metapool and update state.""" 163 | # append pool to list of pools 164 | meta_pool = Pool.meta_pool(address, meta_coins, lp_token, base_coins) 165 | self.pools.append(meta_pool) 166 | 167 | # register coins 168 | self._register_coins(meta_coins) 169 | 170 | # register coin pairs 171 | meta_pairs = it.combinations(meta_coins, 2) 172 | meta_base_pairs = ((m_coin, b_coin) for m_coin in meta_coins[:-1] for b_coin in base_coins) 173 | self._register_coin_pairs(it.chain(meta_pairs, meta_base_pairs)) 174 | 175 | # update timestamp 176 | self.last_updated = timestamp 177 | 178 | def _remove_pool(self, pool: Pool, timestamp: int): 179 | """Remove pool and update state.""" 180 | 181 | wrapped_coins = pool.coins 182 | underlying_coins = pool.underlying_coins 183 | 184 | # unregister coins 185 | self._unregister_coins(it.chain(wrapped_coins, underlying_coins)) 186 | 187 | # unregister coin pairs 188 | wrapped_pairs = it.combinations(wrapped_coins, 2) 189 | underlying_pairs = it.combinations(underlying_coins, 2) 190 | self._unregister_coin_pairs(it.chain(wrapped_pairs, underlying_pairs)) 191 | 192 | # update timestamp 193 | self.last_updated = timestamp 194 | 195 | def _remove_pool_without_underlying(self, pool: Pool, timestamp: int): 196 | """Remove pool and update state.""" 197 | 198 | coins = pool.coins 199 | 200 | # unregister coins 201 | self._unregister_coins(coins) 202 | 203 | # unregister coin pairs 204 | coin_pairs = it.combinations(coins, 2) 205 | self._unregister_coin_pairs(coin_pairs) 206 | 207 | # update timestamp 208 | self.last_updated = timestamp 209 | 210 | def _remove_metapool(self, pool: Pool, timestamp: int): 211 | """Remove metapool and update state.""" 212 | 213 | meta_coins = pool.coins 214 | base_coins = pool.base_coins 215 | 216 | # unregister coins 217 | self._unregister_coins(meta_coins) 218 | 219 | # unregister coin pairs 220 | meta_pairs = it.combinations(meta_coins, 2) 221 | meta_base_pairs = ((m_coin, b_coin) for m_coin in meta_coins[:-1] for b_coin in base_coins) 222 | self._unregister_coin_pairs(it.chain(meta_pairs, meta_base_pairs)) 223 | 224 | # update timestamp 225 | self.last_updated = timestamp 226 | 227 | def remove_pool(self, pool: Pool, timestamp: int): 228 | """Remove a pool and update state.""" 229 | if pool.pool_type == PoolType.BASE: 230 | self._remove_pool_without_underlying(pool, timestamp) 231 | elif pool.pool_type == PoolType.LENDING: 232 | self._remove_pool(pool, timestamp) 233 | else: 234 | self._remove_metapool(pool, timestamp) 235 | -------------------------------------------------------------------------------- /tests/local/integration/test_calculator_dxdy.py: -------------------------------------------------------------------------------- 1 | from brownie.test import given, strategy 2 | from hypothesis import Phase, settings 3 | 4 | 5 | @given( 6 | st_precision=strategy("uint[4]", min_value=6, max_value=18), 7 | st_balance=strategy("uint[4]", min_value=10**21, max_value=10**23, unique=True), 8 | st_rates=strategy("uint[4]", min_value=10**18, max_value=10**19, unique=True), 9 | st_idx=strategy("uint[2]", max_value=3, unique=True), 10 | dx=strategy("uint", min_value=10**19, max_value=10**20), 11 | ) 12 | @settings(phases=[Phase.reuse, Phase.generate]) 13 | def test_dy_dx(calculator, st_precision, st_balance, st_rates, st_idx, dx): 14 | precision = [10 ** (18 - i) for i in st_precision] + [0, 0, 0, 0] 15 | balances = [st_balance[i] // precision[i] for i in range(4)] + [0, 0, 0, 0] 16 | rates = st_rates + [0, 0, 0, 0] 17 | dx //= precision[st_idx[0]] 18 | 19 | dy = calculator.get_dy( 20 | 4, balances, 100, 4000000, rates, precision, st_idx[0], st_idx[1], [dx] + [0] * 99 21 | )[0] 22 | 23 | dx_final = calculator.get_dx( 24 | 4, balances, 100, 4000000, rates, precision, st_idx[0], st_idx[1], dy 25 | ) 26 | 27 | assert min(dx, dx_final) / max(dx, dx_final) >= 0.9995 28 | 29 | 30 | @given( 31 | st_precision=strategy("uint[4]", min_value=6, max_value=18), 32 | st_balance=strategy("uint[4]", min_value=10**21, max_value=10**23, unique=True), 33 | st_rates=strategy("uint[4]", min_value=10**18, max_value=10**19, unique=True), 34 | st_idx=strategy("uint[2]", max_value=3, unique=True), 35 | dy=strategy("uint", min_value=10**19, max_value=10**20), 36 | ) 37 | @settings(phases=[Phase.reuse, Phase.generate]) 38 | def test_dx_dy(calculator, st_precision, st_balance, st_rates, st_idx, dy): 39 | precision = [10 ** (18 - i) for i in st_precision] + [0, 0, 0, 0] 40 | balances = [st_balance[i] // precision[i] for i in range(4)] + [0, 0, 0, 0] 41 | rates = st_rates + [0, 0, 0, 0] 42 | dy //= precision[st_idx[1]] 43 | 44 | dx = calculator.get_dx(4, balances, 100, 4000000, rates, precision, st_idx[0], st_idx[1], dy) 45 | 46 | dy_final = calculator.get_dy( 47 | 4, balances, 100, 4000000, rates, precision, st_idx[0], st_idx[1], [dx] + [0] * 99 48 | )[0] 49 | 50 | assert min(dy, dy_final) / max(dy, dy_final) >= 0.9995 51 | -------------------------------------------------------------------------------- /tests/local/integration/test_simulate_coin_registration.py: -------------------------------------------------------------------------------- 1 | """Test the effect of adding and removing pools on coin registration. 2 | 3 | With the new `swappable_coins` and `swap_coin_for` functions we now have the ability to 4 | iterate through the coins registered in curve pools (base pools, lending pools, and meta pools). 5 | We can also iterate through the pairings which users can swap a given coin (coin a) against 6 | (coin b). 7 | 8 | With the unit tests we have confirmed basic functionality of removing pools and adding them, 9 | however to further verify the functionality this stateful test will continually add and subtract 10 | pools, thereby verifying that functionality isn't lost as more pools are added/subtracted. 11 | """ 12 | from typing import List 13 | 14 | from brownie.network.account import Account 15 | from brownie.network.contract import Contract, ContractContainer 16 | from brownie.network.state import Chain 17 | from brownie.test import strategy 18 | from coin_register_utils import Pool, PoolType, Registry 19 | 20 | from scripts.utils import pack_values 21 | 22 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 23 | 24 | 25 | class BaseHelper: 26 | def __init__( 27 | cls, 28 | alice: Account, 29 | chain: Chain, 30 | ERC20: ContractContainer, 31 | cERC20: ContractContainer, 32 | PoolMockV2: ContractContainer, 33 | MetaPoolMock: ContractContainer, 34 | ): 35 | cls.alice = alice 36 | cls.chain = chain 37 | cls.ERC20 = ERC20 38 | cls.cERC20 = cERC20 39 | cls.PoolMockV2 = PoolMockV2 40 | cls.MetaPoolMock = MetaPoolMock 41 | 42 | cls.tx_params = {"from": alice} 43 | 44 | def _deploy_erc20( 45 | self, name: str = "Test Token", symbol: str = "TST", decimals: int = 18 46 | ) -> Contract: 47 | """Deploy an ERC20 instance.""" 48 | count = len(self.ERC20) 49 | name, symbol = f"{name} {count}", f"{symbol} {count}" 50 | return self.ERC20.deploy(name, symbol, decimals, self.tx_params) 51 | 52 | def _batch_deploy_erc20(self, amount: int = 3) -> List[Contract]: 53 | """Deploy more than one token at once using defaults.""" 54 | return [self._deploy_erc20() for _ in range(amount)] 55 | 56 | def _deploy_wrapped_erc20(self, erc20: Contract) -> Contract: 57 | """Deploy an instance of a wrapped coin.""" 58 | count = len(self.cERC20) 59 | name, symbol, decimals = f"Wrapped Token {count}", f"wTST {count}", 18 60 | return self.cERC20.deploy(name, symbol, decimals, erc20, 10**18, self.tx_params) 61 | 62 | def _batch_deploy_wrapped_erc20(self, erc20s: List[Contract]) -> List[Contract]: 63 | """Batch deploy a set of wrapped ERC20 contracts.""" 64 | return [self._deploy_wrapped_erc20(erc20) for erc20 in erc20s] 65 | 66 | def _deploy_base_pool(self, coins: List[Contract]) -> Contract: 67 | """Deploy a base pool (no underlying coins).""" 68 | n_coins = len(coins) 69 | coins = coins + [ZERO_ADDRESS] * (4 - n_coins) 70 | 71 | return self.PoolMockV2.deploy( 72 | n_coins, coins, [ZERO_ADDRESS] * 4, 70, 4000000, self.tx_params 73 | ) 74 | 75 | def _deploy_lending_pool( 76 | self, coins: List[Contract], underlying_coins: List[Contract] 77 | ) -> Contract: 78 | """Deploy a lending pool with wrapped and underlying coins.""" 79 | n_coins = len(underlying_coins) 80 | coins = coins + [ZERO_ADDRESS] * (4 - len(coins)) 81 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - n_coins) 82 | 83 | return self.PoolMockV2.deploy(n_coins, coins, underlying_coins, 70, 4000000, self.tx_params) 84 | 85 | def _deploy_meta_pool( 86 | self, meta_coins: List[Contract], underlying_coins: List[Contract], base_pool: Contract 87 | ) -> Contract: 88 | """Deploy a meta pool, with a base pool (underlying coins are coins in base).""" 89 | n_metacoins = len(meta_coins) 90 | n_coins = len(underlying_coins) 91 | 92 | meta_coins = meta_coins + [ZERO_ADDRESS] * (4 - n_metacoins) 93 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * (4 - n_coins) 94 | 95 | return self.MetaPoolMock.deploy( 96 | n_metacoins, 97 | n_coins, 98 | base_pool, 99 | meta_coins, 100 | underlying_coins, 101 | 70, 102 | 4000000, 103 | self.tx_params, 104 | ) 105 | 106 | 107 | class StateMachine(BaseHelper): 108 | 109 | st_sleep = strategy("uint256", max_value=86400) 110 | st_random = strategy("uint256", max_value=2**32) 111 | 112 | def __init__(cls, registry: Contract, *args, **kwargs): 113 | super().__init__(cls, *args, **kwargs) 114 | 115 | cls.registry = registry 116 | 117 | def setup(self): 118 | self.state = Registry() 119 | 120 | def rule_add_pool_without_underlying(self): 121 | # deploy 3 coins 122 | coins = self._batch_deploy_erc20() 123 | # number of coins 124 | n_coins = len(coins) 125 | # deploy the base pool 126 | base_pool = self._deploy_base_pool(coins) 127 | # make an lp token 128 | lp_token = self._deploy_erc20() 129 | 130 | # add the pool on chain 131 | tx = self.registry.add_pool_without_underlying( 132 | base_pool, # the swap, but I call it a pool 133 | n_coins, 134 | lp_token, 135 | "0x00", 136 | pack_values([18] * n_coins), 137 | 0, 138 | hasattr(base_pool, "initial_A"), 139 | False, # is_v1 140 | "", 141 | self.tx_params, 142 | ) 143 | 144 | # update our state 145 | self.state.add_pool_without_underlying( 146 | base_pool.address, list(map(str, coins)), lp_token.address, tx.timestamp 147 | ) 148 | 149 | def rule_add_pool(self): 150 | # deploy 3 coins 151 | underlying_coins = self._batch_deploy_erc20() 152 | # deploy wrapped coins 153 | wrapped_coins = self._batch_deploy_wrapped_erc20(underlying_coins) 154 | # coin amount 155 | n_coins = len(wrapped_coins) 156 | # deploy pool/swap 157 | lending_pool = self._deploy_lending_pool(wrapped_coins, underlying_coins) 158 | # lp token 159 | lp_token = self._deploy_erc20() 160 | # rate method id 161 | rate_method_id = self.cERC20.signatures["exchangeRateStored"] 162 | 163 | # add the pool on chain 164 | tx = self.registry.add_pool( 165 | lending_pool, 166 | n_coins, 167 | lp_token, 168 | rate_method_id, 169 | pack_values([18] * n_coins), 170 | pack_values([18] * n_coins), 171 | hasattr(lending_pool, "initial_A"), 172 | False, 173 | "", 174 | self.tx_params, 175 | ) 176 | 177 | # update state 178 | self.state.add_pool( 179 | lending_pool.address, 180 | list(map(str, wrapped_coins)), 181 | list(map(str, underlying_coins)), 182 | lp_token.address, 183 | tx.timestamp, 184 | ) 185 | 186 | def rule_add_metapool(self, st_random): 187 | if len(self.state.base_pools) == 0: 188 | return 189 | 190 | # select a random base pool 191 | pool_index = st_random % len(self.state.base_pools) 192 | # base_pool 193 | base_pool: Pool = self.state.base_pools[pool_index] 194 | # deploy our coins 195 | meta_coins = self._batch_deploy_erc20() + [base_pool.lp_token] 196 | # number of coins 197 | n_coins = len(meta_coins) 198 | 199 | # deploy meta_pool 200 | meta_pool = self._deploy_meta_pool(meta_coins, base_pool.coins, base_pool.address) 201 | # lp token 202 | lp_token = self._deploy_erc20() 203 | 204 | # add the pool on chain 205 | tx = self.registry.add_metapool( 206 | meta_pool, n_coins, lp_token, pack_values([18] * n_coins), "", self.tx_params 207 | ) 208 | 209 | # update state 210 | self.state.add_metapool( 211 | meta_pool.address, 212 | list(map(str, meta_coins)), 213 | lp_token.address, 214 | base_pool.coins, 215 | tx.timestamp, 216 | ) 217 | 218 | def rule_chain_sleep(self, st_sleep): 219 | self.chain.sleep(st_sleep) 220 | 221 | def rule_remove_pool(self, st_random): 222 | if len(self.state.pools) == 0: 223 | return 224 | # select a random pool 225 | random_index = st_random % len(self.state.pools) 226 | 227 | # verify our pool isn't the base of any meta_pools pools 228 | pool: Pool = self.state.pools[random_index] 229 | 230 | # if our pool is the base of a meta pool don't remove it 231 | if pool.pool_type == PoolType.BASE: 232 | for _pool in self.state.meta_pools: 233 | if pool.lp_token in _pool.coins: 234 | return 235 | 236 | # remove from on chain registry 237 | tx = self.registry.remove_pool(pool.address) 238 | 239 | # update our state 240 | self.state.remove_pool(pool, tx.timestamp) 241 | self.state.pools.remove(pool) 242 | 243 | def invariant_coin_count(self): 244 | assert self.registry.coin_count() == self.state.coin_count 245 | 246 | def invariant_get_all_swappable_coins(self): 247 | registered_coins = {self.registry.get_coin(i) for i in range(self.state.coin_count)} 248 | 249 | assert registered_coins == self.state.get_coin 250 | 251 | def invariant_coin_swap_count(self): 252 | for coin in self.state.get_coin: 253 | assert self.registry.get_coin_swap_count(coin) == self.state.get_coin_swap_count[coin] 254 | 255 | def invariant_swap_coin_for(self): 256 | for coin, expected_coin_set in self.state.get_coin_swap_complement.items(): 257 | coin_set = { 258 | self.registry.get_coin_swap_complement(coin, i) 259 | for i in range(self.state.get_coin_swap_count[coin]) 260 | } 261 | assert coin_set == expected_coin_set 262 | 263 | def invariant_last_updated(self): 264 | assert self.state.last_updated == self.registry.last_updated() 265 | 266 | 267 | def test_simulate_coin_registry( 268 | state_machine, registry, alice, chain, ERC20, cERC20, PoolMockV2, MetaPoolMock 269 | ): 270 | state_machine( 271 | StateMachine, 272 | registry, 273 | alice, 274 | chain, 275 | ERC20, 276 | cERC20, 277 | PoolMockV2, 278 | MetaPoolMock, 279 | # settings={"stateful_step_count": 25}, 280 | ) 281 | -------------------------------------------------------------------------------- /tests/local/unitary/AddressProvider/test_add_new_id.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | 4 | def test_assumption_initial_max_id(provider): 5 | assert provider.max_id() == 0 6 | 7 | 8 | def test_add_and_get_info(provider, registry_pool_info, alice): 9 | tx = provider.add_new_id(registry_pool_info, "Pool Info", {"from": alice}) 10 | 11 | info = provider.get_id_info(1) 12 | 13 | assert info["addr"] == registry_pool_info 14 | assert info["description"] == "Pool Info" 15 | assert info["version"] == 1 16 | assert info["is_active"] is True 17 | assert info["last_modified"] == tx.timestamp 18 | 19 | 20 | def test_add_multiple(provider, registry, alice): 21 | last_description = "Main Registry" 22 | 23 | for i in range(1, 6): 24 | description = f"foobar{i}" 25 | provider.add_new_id(registry, description, {"from": alice}) 26 | 27 | assert provider.max_id() == i 28 | assert provider.get_id_info(i)["description"] == description 29 | assert provider.get_id_info(i - 1)["description"] == last_description 30 | 31 | last_description = description 32 | 33 | 34 | def test_admin_only(provider, registry, bob): 35 | with brownie.reverts("dev: admin-only function"): 36 | provider.add_new_id(registry, "", {"from": bob}) 37 | 38 | 39 | def test_contract_only(provider, alice): 40 | with brownie.reverts("dev: not a contract"): 41 | provider.add_new_id(alice, "", {"from": alice}) 42 | -------------------------------------------------------------------------------- /tests/local/unitary/AddressProvider/test_admin.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | 4 | def test_set_admin_on_deployment(AddressProvider, alice, bob): 5 | provider = AddressProvider.deploy(alice, {"from": alice}) 6 | assert provider.admin() == alice 7 | 8 | provider = AddressProvider.deploy(bob, {"from": alice}) 9 | assert provider.admin() == bob 10 | 11 | 12 | def test_transfer_ownership(alice, bob, chain, provider): 13 | provider.commit_transfer_ownership(bob, {"from": alice}) 14 | assert provider.admin() == alice 15 | 16 | chain.sleep(3 * 86400) 17 | provider.apply_transfer_ownership({"from": alice}) 18 | 19 | assert provider.admin() == bob 20 | 21 | 22 | def test_time_delay(alice, bob, chain, provider): 23 | provider.commit_transfer_ownership(bob, {"from": alice}) 24 | 25 | # immediate 26 | with brownie.reverts("dev: now < deadline"): 27 | provider.apply_transfer_ownership({"from": alice}) 28 | 29 | chain.sleep(86400) 30 | with brownie.reverts("dev: now < deadline"): 31 | provider.apply_transfer_ownership({"from": alice}) 32 | 33 | chain.sleep(86400) 34 | with brownie.reverts("dev: now < deadline"): 35 | provider.apply_transfer_ownership({"from": alice}) 36 | 37 | chain.sleep(86400) 38 | provider.apply_transfer_ownership({"from": alice}) 39 | 40 | 41 | def test_revert_transfer(alice, bob, chain, provider): 42 | provider.commit_transfer_ownership(bob, {"from": alice}) 43 | 44 | chain.sleep(3 * 86400) 45 | provider.revert_transfer_ownership({"from": alice}) 46 | 47 | with brownie.reverts("dev: transfer not active"): 48 | provider.apply_transfer_ownership({"from": alice}) 49 | 50 | 51 | def test_commit_already_pending(alice, bob, provider): 52 | provider.commit_transfer_ownership(bob, {"from": alice}) 53 | 54 | with brownie.reverts("dev: transfer already active"): 55 | provider.commit_transfer_ownership(bob, {"from": alice}) 56 | 57 | 58 | def test_commit_admin_only(bob, provider): 59 | with brownie.reverts("dev: admin-only function"): 60 | provider.commit_transfer_ownership(bob, {"from": bob}) 61 | 62 | 63 | def test_apply_admin_only(bob, provider): 64 | with brownie.reverts("dev: admin-only function"): 65 | provider.apply_transfer_ownership({"from": bob}) 66 | 67 | 68 | def test_revert_admin_only(bob, provider): 69 | with brownie.reverts("dev: admin-only function"): 70 | provider.revert_transfer_ownership({"from": bob}) 71 | 72 | 73 | def test_transfer_twice(alice, bob, chain, provider): 74 | provider.commit_transfer_ownership(bob, {"from": alice}) 75 | 76 | chain.sleep(3 * 86400) 77 | provider.apply_transfer_ownership({"from": alice}) 78 | provider.commit_transfer_ownership(bob, {"from": bob}) 79 | 80 | chain.sleep(3 * 86400) 81 | provider.apply_transfer_ownership({"from": bob}) 82 | assert provider.admin() == bob 83 | -------------------------------------------------------------------------------- /tests/local/unitary/AddressProvider/test_set_unset.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | 7 | @pytest.fixture(scope="module", autouse=True) 8 | def setup(provider, registry_pool_info, registry_swap, alice): 9 | provider.add_new_id(registry_pool_info, "pool info", {"from": alice}) 10 | provider.add_new_id(registry_swap, "swap", {"from": alice}) 11 | 12 | 13 | def test_initial_id_info_1(provider, registry_pool_info): 14 | info = provider.get_id_info(1) 15 | 16 | assert info["description"] == "pool info" 17 | assert info["version"] == 1 18 | assert info["is_active"] is True 19 | assert info["addr"] == registry_pool_info 20 | 21 | 22 | def test_initial_id_info_2(provider, registry_swap): 23 | info = provider.get_id_info(2) 24 | 25 | assert info["description"] == "swap" 26 | assert info["version"] == 1 27 | assert info["is_active"] is True 28 | assert info["addr"] == registry_swap 29 | 30 | 31 | def test_set(provider, registry_swap, registry, alice): 32 | tx = provider.set_address(1, registry_swap, {"from": alice}) 33 | info = provider.get_id_info(1) 34 | 35 | assert info["description"] == "pool info" 36 | assert info["version"] == 2 37 | assert info["is_active"] is True 38 | assert info["last_modified"] == tx.timestamp 39 | assert info["addr"] == registry_swap 40 | 41 | 42 | def test_unset(provider, registry, alice): 43 | tx = provider.unset_address(1, {"from": alice}) 44 | info = provider.get_id_info(1) 45 | 46 | assert info["description"] == "pool info" 47 | assert info["version"] == 1 48 | assert info["is_active"] is False 49 | assert info["last_modified"] == tx.timestamp 50 | assert info["addr"] == ZERO_ADDRESS 51 | 52 | 53 | def test_registry_not_affected(provider, registry, registry_swap, alice): 54 | provider.set_address(1, registry_swap, {"from": alice}) 55 | provider.set_address(2, registry_swap, {"from": alice}) 56 | 57 | assert provider.get_registry() == registry 58 | 59 | 60 | def test_set_admin_only(provider, registry, bob): 61 | with brownie.reverts("dev: admin-only function"): 62 | provider.set_address(1, registry, {"from": bob}) 63 | 64 | 65 | def test_unset_admin_only(provider, bob): 66 | with brownie.reverts("dev: admin-only function"): 67 | provider.unset_address(1, {"from": bob}) 68 | 69 | 70 | def test_set_contract_only(provider, alice): 71 | with brownie.reverts("dev: not a contract"): 72 | provider.set_address(1, alice, {"from": alice}) 73 | 74 | 75 | def test_set_beyond_max_id(provider, registry, alice): 76 | max_id = provider.max_id() 77 | with brownie.reverts("dev: id does not exist"): 78 | provider.set_address(max_id + 1, registry, {"from": alice}) 79 | 80 | 81 | def test_unset_beyond_max_id(provider, alice): 82 | max_id = provider.max_id() 83 | with brownie.reverts("dev: not active"): 84 | provider.unset_address(max_id + 1, {"from": alice}) 85 | 86 | 87 | def test_unset_inactive(provider, alice): 88 | max_id = provider.max_id() 89 | provider.unset_address(max_id, {"from": alice}) 90 | 91 | with brownie.reverts("dev: not active"): 92 | provider.unset_address(max_id, {"from": alice}) 93 | -------------------------------------------------------------------------------- /tests/local/unitary/AddressProvider/test_set_unset_registry.py: -------------------------------------------------------------------------------- 1 | from brownie import ZERO_ADDRESS 2 | 3 | 4 | def test_assumptions_registry_is_unset(provider): 5 | assert provider.get_registry() == ZERO_ADDRESS 6 | assert provider.get_address(0) == ZERO_ADDRESS 7 | 8 | 9 | def test_initial_id_info(provider): 10 | info = provider.get_id_info(0) 11 | 12 | assert info["description"] == "Main Registry" 13 | assert info["version"] == 0 14 | assert info["is_active"] is False 15 | 16 | 17 | def test_set_registry(provider, registry, alice): 18 | provider.set_address(0, registry, {"from": alice}) 19 | 20 | assert provider.get_registry() == registry 21 | assert provider.get_address(0) == registry 22 | 23 | 24 | def test_unset_registry(provider, registry, alice): 25 | provider.set_address(0, registry, {"from": alice}) 26 | provider.unset_address(0, {"from": alice}) 27 | 28 | assert provider.get_registry() == ZERO_ADDRESS 29 | assert provider.get_address(0) == ZERO_ADDRESS 30 | 31 | 32 | def test_unset_and_set_again(provider, registry, alice): 33 | provider.set_address(0, registry, {"from": alice}) 34 | provider.unset_address(0, {"from": alice}) 35 | provider.set_address(0, registry, {"from": alice}) 36 | 37 | assert provider.get_registry() == registry 38 | assert provider.get_address(0) == registry 39 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_add_pool_get_decimals.py: -------------------------------------------------------------------------------- 1 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 2 | 3 | 4 | def test_get_decimals_lending( 5 | alice, 6 | registry, 7 | lending_swap, 8 | lp_token, 9 | n_coins, 10 | is_v1, 11 | rate_method_id, 12 | underlying_decimals, 13 | wrapped_decimals, 14 | ): 15 | registry.add_pool( 16 | lending_swap, 17 | n_coins, 18 | lp_token, 19 | rate_method_id, 20 | 0, 21 | 0, 22 | hasattr(lending_swap, "initial_A"), 23 | is_v1, 24 | "", 25 | {"from": alice}, 26 | ) 27 | zero_pad = [0] * (8 - n_coins) 28 | 29 | assert registry.get_decimals(lending_swap) == wrapped_decimals + zero_pad 30 | assert registry.get_underlying_decimals(lending_swap) == underlying_decimals + zero_pad 31 | 32 | 33 | def test_get_decimals(alice, registry, swap, lp_token, n_coins, is_v1, underlying_decimals): 34 | registry.add_pool_without_underlying( 35 | swap, 36 | n_coins, 37 | lp_token, 38 | "0x00", 39 | 0, 40 | 0, # use rates 41 | hasattr(swap, "initial_A"), 42 | is_v1, 43 | "", 44 | {"from": alice}, 45 | ) 46 | 47 | assert registry.get_decimals(swap) == underlying_decimals + [0] * (8 - n_coins) 48 | 49 | 50 | def test_get_decimals_metapool( 51 | alice, 52 | registry, 53 | swap, 54 | meta_swap, 55 | lp_token, 56 | meta_lp_token, 57 | n_coins, 58 | n_metacoins, 59 | is_v1, 60 | meta_decimals, 61 | underlying_decimals, 62 | ): 63 | registry.add_pool_without_underlying( 64 | swap, 65 | n_coins, 66 | lp_token, 67 | "0x00", 68 | 0, 69 | 0, # use rates 70 | hasattr(swap, "initial_A"), 71 | is_v1, 72 | "", 73 | {"from": alice}, 74 | ) 75 | registry.add_metapool(meta_swap, n_metacoins, meta_lp_token, 0, "", {"from": alice}) 76 | 77 | expected = meta_decimals[:-1] + underlying_decimals 78 | expected += [0] * (8 - len(expected)) 79 | 80 | assert registry.get_decimals(meta_swap) == meta_decimals + [0] * (8 - n_metacoins) 81 | assert registry.get_underlying_decimals(meta_swap) == expected 82 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_get_pool_asset_type.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | from scripts.utils import pack_values 5 | 6 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 7 | 8 | 9 | @pytest.fixture(autouse=True) 10 | def setup(registry, alice, swap, lp_token, n_coins, is_v1, underlying_decimals): 11 | registry.add_pool_without_underlying( 12 | swap, 13 | n_coins, 14 | lp_token, 15 | "0x00", 16 | pack_values(underlying_decimals), 17 | 0, # use rates 18 | hasattr(swap, "initial_A"), 19 | is_v1, 20 | "Base Swap", 21 | {"from": alice}, 22 | ) 23 | 24 | 25 | @pytest.mark.once 26 | def test_set_pool_asset_type(registry, alice, swap): 27 | tx = registry.set_pool_asset_type(swap, 42, {"from": alice}) 28 | 29 | assert registry.get_pool_asset_type(swap) == 42 30 | assert registry.last_updated() == tx.timestamp 31 | 32 | 33 | @pytest.mark.once 34 | def test_get_pool_asset_type_reverts_on_fail(registry, bob): 35 | with brownie.reverts("dev: admin-only function"): 36 | registry.set_pool_asset_type(ZERO_ADDRESS, 42, {"from": bob}) 37 | 38 | 39 | @pytest.mark.once 40 | def test_batch_set_pool_asset_type(registry, alice, swap): 41 | asset_types = [42] + [0] * 31 42 | tx = registry.batch_set_pool_asset_type( 43 | [swap] + [ZERO_ADDRESS] * 31, asset_types, {"from": alice} 44 | ) 45 | 46 | assert registry.get_pool_asset_type(swap) == 42 47 | assert registry.last_updated() == tx.timestamp 48 | 49 | 50 | @pytest.mark.once 51 | def test_asset_type_removed_after_pool_removal(registry, swap, alice): 52 | registry.set_pool_asset_type(swap, 42, {"from": alice}) 53 | registry.remove_pool(swap, {"from": alice}) 54 | 55 | assert registry.get_pool_asset_type(swap) == 0 56 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_get_rate_alt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ankrETH 3 | from brownie.test import given, strategy 4 | 5 | from scripts.utils import pack_values 6 | 7 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def ankr_swap(PoolMockV2, alice): 12 | underlying_coins = ["0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"] 13 | wrapped_coins = [ankrETH.deploy({"from": alice})] + [ZERO_ADDRESS] * 3 14 | underlying_coins = underlying_coins + [ZERO_ADDRESS] * 3 15 | 16 | contract = PoolMockV2.deploy(1, wrapped_coins, underlying_coins, 70, 4000000, {"from": alice}) 17 | return contract 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def registry(ERC20, Registry, RateCalcMock, provider, gauge_controller, alice, ankr_swap, lp_token): 22 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 23 | rate_calc = RateCalcMock.deploy({"from": alice}) 24 | 25 | registry.add_pool( 26 | ankr_swap, 27 | 1, 28 | lp_token, 29 | rate_calc.address + "71ca337d", 30 | pack_values([18]), 31 | pack_values([18]), 32 | hasattr(ankr_swap, "initial_A"), 33 | False, 34 | "Test pool", 35 | {"from": alice}, 36 | ) 37 | provider.set_address(0, registry, {"from": alice}) 38 | yield registry 39 | 40 | 41 | @given(new_ratio=strategy("uint256", min_value=1, max_value=10**18 - 1)) 42 | def test_get_rates(alice, registry, registry_pool_info, new_ratio, ankr_swap): 43 | ankrETH[0].update_ratio(new_ratio, {"from": alice}) 44 | 45 | rates = [0] * 8 46 | rates[0] = (10**36) // new_ratio 47 | 48 | assert registry.get_rates(ankr_swap) == rates 49 | assert registry_pool_info.get_pool_info(ankr_swap)["rates"] == rates 50 | 51 | 52 | def test_call_to_rate_calculator(alice, ankr_swap, registry, RateCalcMock): 53 | rate_calc = RateCalcMock[0] 54 | coin = ankrETH[0] 55 | tx = registry.get_rates.transact(ankr_swap, {"from": alice}) 56 | 57 | expected = { 58 | "function": "get_rate(address)", 59 | "inputs": dict(_coin=coin.address), 60 | "op": "STATICCALL", 61 | "to": rate_calc.address, 62 | } 63 | assert all(tx.subcalls[0][k] == expected[k] for k in expected.keys()) 64 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_getters_no_lending.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | import math 3 | from collections import Counter, defaultdict 4 | 5 | import pytest 6 | 7 | from scripts.utils import pack_values 8 | 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | 11 | 12 | @pytest.fixture(scope="module", autouse=True) 13 | def registry( 14 | Registry, provider, gauge_controller, alice, swap, lp_token, n_coins, is_v1, underlying_decimals 15 | ): 16 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 17 | registry.add_pool_without_underlying( 18 | swap, 19 | n_coins, 20 | lp_token, 21 | "0x00", 22 | pack_values(underlying_decimals), 23 | 0, # use rates 24 | hasattr(swap, "initial_A"), 25 | is_v1, 26 | "Base Swap", 27 | {"from": alice}, 28 | ) 29 | provider.set_address(0, registry, {"from": alice}) 30 | yield registry 31 | 32 | 33 | @pytest.mark.itercoins("send", "recv") 34 | def test_find_pool(registry, swap, underlying_coins, send, recv): 35 | assert registry.find_pool_for_coins(underlying_coins[send], underlying_coins[recv]) == swap 36 | 37 | 38 | @pytest.mark.itercoins("idx") 39 | def test_find_pool_not_exists(registry, swap, underlying_coins, idx): 40 | assert ( 41 | registry.find_pool_for_coins(underlying_coins[idx], underlying_coins[idx]) == ZERO_ADDRESS 42 | ) 43 | 44 | 45 | def test_get_n_coins(registry, swap, n_coins): 46 | assert registry.get_n_coins(swap) == [n_coins, n_coins] 47 | 48 | 49 | def test_get_coins(registry, swap, underlying_coins, n_coins): 50 | expected = underlying_coins + [ZERO_ADDRESS] * (8 - n_coins) 51 | assert registry.get_coins(swap) == expected 52 | assert registry.get_underlying_coins(swap) == expected 53 | 54 | 55 | def test_get_decimals(registry, registry_pool_info, swap, underlying_decimals, n_coins): 56 | expected = underlying_decimals + [0] * (8 - n_coins) 57 | assert registry.get_decimals(swap) == expected 58 | assert registry.get_underlying_decimals(swap) == expected 59 | 60 | pool_info = registry_pool_info.get_pool_info(swap) 61 | assert pool_info["decimals"] == expected 62 | assert pool_info["underlying_decimals"] == expected 63 | 64 | 65 | def test_get_pool_coins(registry_pool_info, swap, underlying_coins, underlying_decimals, n_coins): 66 | coin_info = registry_pool_info.get_pool_coins(swap) 67 | assert coin_info["coins"] == underlying_coins + [ZERO_ADDRESS] * (8 - n_coins) 68 | assert coin_info["underlying_coins"] == underlying_coins + [ZERO_ADDRESS] * (8 - n_coins) 69 | assert coin_info["decimals"] == underlying_decimals + [0] * (8 - n_coins) 70 | assert coin_info["underlying_decimals"] == underlying_decimals + [0] * (8 - n_coins) 71 | 72 | 73 | def test_get_rates(registry, registry_pool_info, swap, n_coins): 74 | expected = [10**18] * n_coins + [0] * (8 - n_coins) 75 | assert registry.get_rates(swap) == expected 76 | assert registry_pool_info.get_pool_info(swap)["rates"] == expected 77 | 78 | 79 | def test_get_balances(registry, registry_pool_info, swap, n_coins): 80 | balances = [1234, 2345, 3456, 4567] 81 | swap._set_balances(balances) 82 | 83 | expected = balances[:n_coins] + [0] * (8 - n_coins) 84 | assert registry.get_balances(swap) == expected 85 | assert registry.get_underlying_balances(swap) == expected 86 | 87 | pool_info = registry_pool_info.get_pool_info(swap) 88 | assert pool_info["balances"] == expected 89 | assert pool_info["underlying_balances"] == expected 90 | 91 | 92 | def test_get_admin_balances(alice, registry, swap, underlying_coins, n_coins): 93 | assert registry.get_admin_balances(swap) == [0] * 8 94 | 95 | expected = [0] * 8 96 | for i, coin in enumerate(underlying_coins, start=1): 97 | expected[i - 1] = 666 * i 98 | if coin == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 99 | alice.transfer(swap, expected[i - 1]) 100 | else: 101 | coin._mint_for_testing(swap, expected[i - 1]) 102 | 103 | assert registry.get_admin_balances(swap) == expected 104 | 105 | swap._set_balances([i // 4 for i in expected[:4]]) 106 | 107 | expected = [i - i // 4 for i in expected] 108 | assert registry.get_admin_balances(swap) == expected 109 | 110 | 111 | @pytest.mark.itercoins("send", "recv") 112 | def test_get_coin_indices(alice, registry, swap, underlying_coins, send, recv): 113 | assert registry.get_coin_indices(swap, underlying_coins[send], underlying_coins[recv]) == ( 114 | send, 115 | recv, 116 | False, 117 | ) 118 | 119 | 120 | @pytest.mark.once 121 | def test_get_A(alice, registry, swap): 122 | assert registry.get_A(swap) == swap.A() 123 | 124 | swap._set_A(12345, 0, 0, 0, 0, {"from": alice}) 125 | assert registry.get_A(swap) == 12345 126 | 127 | 128 | @pytest.mark.once 129 | def test_get_fees(alice, registry, swap): 130 | assert registry.get_fees(swap) == [swap.fee(), swap.admin_fee()] 131 | 132 | swap._set_fees_and_owner(12345, 31337, 0, 0, alice, {"from": alice}) 133 | assert registry.get_fees(swap) == [12345, 31337] 134 | 135 | 136 | @pytest.mark.once 137 | def test_get_virtual_price_from_lp_token(alice, registry, swap, lp_token): 138 | assert registry.get_virtual_price_from_lp_token(lp_token) == 10**18 139 | swap._set_virtual_price(12345678, {"from": alice}) 140 | assert registry.get_virtual_price_from_lp_token(lp_token) == 12345678 141 | 142 | 143 | @pytest.mark.once 144 | def test_get_pool_from_lp_token(registry, swap, lp_token): 145 | assert registry.get_pool_from_lp_token(lp_token) == swap 146 | 147 | 148 | @pytest.mark.once 149 | def test_get_lp_token(registry, swap, lp_token): 150 | assert registry.get_lp_token(swap) == lp_token 151 | 152 | 153 | def test_coin_count_is_correct(registry, underlying_coins): 154 | coin_set = set(map(str, underlying_coins)) 155 | 156 | assert registry.coin_count() == len(coin_set) 157 | 158 | 159 | def test_get_all_swappable_coins(registry, underlying_coins): 160 | expected_coin_set = set(map(str, underlying_coins)) 161 | coin_count = registry.coin_count() 162 | 163 | coins = set(registry.get_coin(i) for i in range(coin_count)) 164 | 165 | assert coins == expected_coin_set 166 | 167 | 168 | @pytest.mark.once 169 | def test_last_updated_getter(registry, history): 170 | registry_txs = history.filter(receiver=registry.address) 171 | assert math.isclose(registry_txs[-1].timestamp, registry.last_updated()) 172 | 173 | 174 | def test_coin_swap_count(registry, underlying_coins): 175 | underlying_coins = set(map(str, underlying_coins)) 176 | pairings = it.combinations(underlying_coins, 2) 177 | counts = Counter(it.chain(*pairings)) 178 | 179 | for coin in underlying_coins: 180 | assert registry.get_coin_swap_count(coin) == counts[coin] 181 | 182 | 183 | def test_swap_coin_for(registry, underlying_coins): 184 | underlying_coins = set(map(str, underlying_coins)) 185 | pairings = it.combinations(underlying_coins, 2) 186 | swaps = defaultdict(set) 187 | 188 | for coina, coinb in pairings: 189 | swaps[coina].add(coinb) 190 | swaps[coinb].add(coina) 191 | 192 | for coin in underlying_coins: 193 | coin_swap_count = registry.get_coin_swap_count(coin) 194 | swap_coins = {registry.get_coin_swap_complement(coin, i) for i in range(coin_swap_count)} 195 | 196 | assert swap_coins == swaps[coin] 197 | 198 | 199 | @pytest.mark.once 200 | def test_is_metapool(registry, swap): 201 | assert registry.is_meta(swap) is False 202 | 203 | 204 | @pytest.mark.once 205 | def test_get_name(registry, swap): 206 | assert registry.get_pool_name(swap) == "Base Swap" 207 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_liquidity_gauges.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | pytestmark = pytest.mark.once 7 | 8 | 9 | @pytest.fixture(scope="module", autouse=True) 10 | def registry(Registry, provider, gauge_controller, alice, swap, lp_token, n_coins, is_v1): 11 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 12 | registry.add_pool_without_underlying( 13 | swap, 14 | n_coins, 15 | lp_token, 16 | "0x00", 17 | 0, 18 | 0, # use rates 19 | hasattr(swap, "initial_A"), 20 | is_v1, 21 | "", 22 | {"from": alice}, 23 | ) 24 | yield registry 25 | 26 | 27 | def test_set_liquidity_gauges(alice, registry, gauge_controller, liquidity_gauge, swap, lp_token): 28 | gauges = [liquidity_gauge] + [ZERO_ADDRESS] * 9 29 | gauge_types = [gauge_controller.gauge_types(liquidity_gauge)] + [0] * 9 30 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 31 | assert registry.get_gauges(swap) == (gauges, gauge_types) 32 | 33 | 34 | def test_incorrect_gauge(LiquidityGaugeMock, alice, registry, gauge_controller, swap): 35 | gauge = LiquidityGaugeMock.deploy(swap, {"from": alice}) 36 | gauges = [gauge] + [ZERO_ADDRESS] * 9 37 | with brownie.reverts("dev: wrong token"): 38 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 39 | 40 | 41 | def test_incorrect_gauge_multiple( 42 | LiquidityGaugeMock, alice, registry, gauge_controller, liquidity_gauge, swap 43 | ): 44 | gauge = LiquidityGaugeMock.deploy(swap, {"from": alice}) 45 | gauges = [liquidity_gauge, gauge] + [ZERO_ADDRESS] * 8 46 | with brownie.reverts("dev: wrong token"): 47 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 48 | 49 | 50 | def test_set_multiple(LiquidityGaugeMock, alice, registry, swap, lp_token, gauge_controller): 51 | gauges = [] 52 | gauge_types = [] 53 | 54 | for i in range(10): 55 | gauge = LiquidityGaugeMock.deploy(lp_token, {"from": alice}) 56 | gauge_controller._set_gauge_type(gauge, i, {"from": alice}) 57 | gauges.append(gauge) 58 | gauge_types.append(i) 59 | 60 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 61 | assert registry.get_gauges(swap) == (gauges, gauge_types) 62 | 63 | 64 | def test_unset_multiple(LiquidityGaugeMock, alice, registry, swap, lp_token, gauge_controller): 65 | gauges = [] 66 | gauge_types = [] 67 | 68 | for i in range(10): 69 | gauge = LiquidityGaugeMock.deploy(lp_token, {"from": alice}) 70 | gauge_controller._set_gauge_type(gauge, i, {"from": alice}) 71 | gauges.append(gauge) 72 | gauge_types.append(i) 73 | 74 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 75 | 76 | gauges = gauges[2:5] + [ZERO_ADDRESS] * 7 77 | gauge_types = gauge_types[2:5] + [0] * 7 78 | registry.set_liquidity_gauges(swap, gauges, {"from": alice}) 79 | 80 | assert registry.get_gauges(swap) == (gauges, gauge_types) 81 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_remove_pool_lending.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import math 3 | from collections import defaultdict 4 | 5 | import brownie 6 | import pytest 7 | 8 | from scripts.utils import pack_values 9 | 10 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 11 | 12 | 13 | @pytest.fixture(scope="module", autouse=True) 14 | def registry( 15 | Registry, 16 | provider, 17 | gauge_controller, 18 | alice, 19 | lending_swap, 20 | lp_token, 21 | n_coins, 22 | is_v1, 23 | rate_method_id, 24 | underlying_decimals, 25 | wrapped_decimals, 26 | chain, 27 | ): 28 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 29 | registry.add_pool( 30 | lending_swap, 31 | n_coins, 32 | lp_token, 33 | rate_method_id, 34 | pack_values(wrapped_decimals), 35 | pack_values(underlying_decimals), 36 | hasattr(lending_swap, "initial_A"), 37 | is_v1, 38 | "", 39 | {"from": alice}, 40 | ) 41 | chain.sleep(10) 42 | registry.remove_pool(lending_swap, {"from": alice}) 43 | yield registry 44 | 45 | 46 | @pytest.mark.itercoins("send", "recv") 47 | def test_find_pool(registry, wrapped_coins, underlying_coins, send, recv): 48 | assert registry.find_pool_for_coins(wrapped_coins[send], wrapped_coins[recv]) == ZERO_ADDRESS 49 | assert ( 50 | registry.find_pool_for_coins(underlying_coins[send], underlying_coins[recv]) == ZERO_ADDRESS 51 | ) 52 | 53 | 54 | def test_get_n_coins(registry, lending_swap): 55 | assert registry.get_n_coins(lending_swap) == [0, 0] 56 | 57 | 58 | def test_get_coins(registry, lending_swap): 59 | assert registry.get_coins(lending_swap) == [ZERO_ADDRESS] * 8 60 | assert registry.get_underlying_coins(lending_swap) == [ZERO_ADDRESS] * 8 61 | 62 | 63 | def test_get_decimals(registry, lending_swap): 64 | assert registry.get_decimals(lending_swap) == [0] * 8 65 | assert registry.get_underlying_decimals(lending_swap) == [0] * 8 66 | 67 | 68 | def test_get_rates(registry, lending_swap): 69 | assert registry.get_rates(lending_swap) == [0] * 8 70 | 71 | 72 | @pytest.mark.itercoins("send", "recv") 73 | def test_get_coin_indices(registry, lending_swap, underlying_coins, wrapped_coins, send, recv): 74 | with brownie.reverts("No available market"): 75 | registry.get_coin_indices(lending_swap, wrapped_coins[send], wrapped_coins[recv]) 76 | with brownie.reverts("No available market"): 77 | registry.get_coin_indices(lending_swap, underlying_coins[send], underlying_coins[recv]) 78 | 79 | 80 | @pytest.mark.once 81 | def test_get_balances(registry, lending_swap): 82 | with brownie.reverts(): 83 | registry.get_balances(lending_swap) 84 | 85 | 86 | @pytest.mark.once 87 | def test_get_underlying_balances(registry, lending_swap): 88 | with brownie.reverts(): 89 | registry.get_underlying_balances(lending_swap) 90 | 91 | 92 | @pytest.mark.once 93 | def test_get_admin_balances(registry, lending_swap): 94 | with brownie.reverts(): 95 | registry.get_admin_balances(lending_swap) 96 | 97 | 98 | @pytest.mark.once 99 | def test_get_virtual_price_from_lp_token(alice, registry, lp_token): 100 | with brownie.reverts(): 101 | registry.get_virtual_price_from_lp_token(lp_token) 102 | 103 | 104 | @pytest.mark.once 105 | def test_get_pool_from_lp_token(registry, lp_token): 106 | assert registry.get_pool_from_lp_token(lp_token) == ZERO_ADDRESS 107 | 108 | 109 | @pytest.mark.once 110 | def test_get_lp_token(registry, lending_swap): 111 | assert registry.get_lp_token(lending_swap) == ZERO_ADDRESS 112 | 113 | 114 | def test_coin_count_is_correct(registry): 115 | 116 | assert registry.coin_count() == 0 117 | 118 | 119 | def test_get_all_swappable_coins(registry, wrapped_coins, underlying_coins): 120 | coin_set = set(map(str, itertools.chain(wrapped_coins, underlying_coins))) 121 | coin_count = len(coin_set) 122 | 123 | coins = set(registry.get_coin(i) for i in range(coin_count)) 124 | 125 | assert coins == {ZERO_ADDRESS} 126 | 127 | 128 | @pytest.mark.once 129 | def test_last_updated_getter(registry, history): 130 | registry_txs = history.filter(receiver=registry.address) 131 | assert math.isclose(registry_txs[-1].timestamp, registry.last_updated()) 132 | 133 | 134 | def test_coin_swap_count(registry, wrapped_coins, underlying_coins): 135 | coins = set(map(str, itertools.chain(wrapped_coins, underlying_coins))) 136 | 137 | for coin in coins: 138 | assert registry.get_coin_swap_count(coin) == 0 139 | 140 | 141 | def test_swap_coin_for(registry, wrapped_coins, underlying_coins): 142 | wrapped_coins = list(map(str, wrapped_coins)) 143 | underlying_coins = list(map(str, underlying_coins)) 144 | pairings = defaultdict(set) 145 | 146 | wrapped_pairs = itertools.combinations(wrapped_coins, 2) 147 | underlying_pairs = itertools.combinations(underlying_coins, 2) 148 | 149 | for coin_a, coin_b in itertools.chain(wrapped_pairs, underlying_pairs): 150 | pairings[coin_a].add(coin_b) 151 | pairings[coin_b].add(coin_a) 152 | 153 | for coin in pairings.keys(): 154 | coin_swap_count = len(pairings[coin]) 155 | available_swaps = { 156 | registry.get_coin_swap_complement(coin, i) for i in range(coin_swap_count) 157 | } 158 | 159 | assert available_swaps == {ZERO_ADDRESS} 160 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_remove_pool_meta.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import math 3 | from collections import Counter, defaultdict 4 | 5 | import brownie 6 | import pytest 7 | 8 | from scripts.utils import pack_values 9 | 10 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 11 | 12 | 13 | @pytest.fixture(scope="module", autouse=True, params=["meta", "factory"]) 14 | def registry( 15 | Registry, 16 | provider, 17 | gauge_controller, 18 | alice, 19 | swap, 20 | meta_swap, 21 | lp_token, 22 | meta_lp_token, 23 | n_coins, 24 | n_metacoins, 25 | is_v1, 26 | underlying_decimals, 27 | meta_decimals, 28 | chain, 29 | request, 30 | ): 31 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 32 | registry.add_pool_without_underlying( 33 | swap, 34 | n_coins, 35 | lp_token, 36 | "0x00", 37 | pack_values(underlying_decimals), 38 | 0, # use rates 39 | hasattr(swap, "initial_A"), 40 | is_v1, 41 | "", 42 | {"from": alice}, 43 | ) 44 | registry.add_metapool( 45 | meta_swap, 46 | n_metacoins, 47 | meta_lp_token, 48 | pack_values(meta_decimals), 49 | "", 50 | ZERO_ADDRESS if request.param == "meta" else swap, 51 | {"from": alice}, 52 | ) 53 | chain.sleep(10) 54 | registry.remove_pool(meta_swap, {"from": alice}) 55 | yield registry 56 | 57 | 58 | def test_find_pool(registry, meta_coins, underlying_coins, n_metacoins): 59 | for i, j in itertools.combinations(range(n_metacoins), 2): 60 | assert registry.find_pool_for_coins(meta_coins[i], meta_coins[j]) == ZERO_ADDRESS 61 | 62 | for meta, base in itertools.product(meta_coins[:-1], underlying_coins): 63 | assert registry.find_pool_for_coins(meta, base) == ZERO_ADDRESS 64 | 65 | 66 | def test_get_n_coins(registry, meta_swap): 67 | assert registry.get_n_coins(meta_swap) == [0, 0] 68 | 69 | 70 | def test_get_coins(registry, meta_swap, meta_coins): 71 | assert registry.get_coins(meta_swap) == [ZERO_ADDRESS] * 8 72 | assert registry.get_underlying_coins(meta_swap) == [ZERO_ADDRESS] * 8 73 | 74 | 75 | def test_get_decimals(registry, meta_swap): 76 | assert registry.get_decimals(meta_swap) == [0] * 8 77 | assert registry.get_underlying_decimals(meta_swap) == [0] * 8 78 | 79 | 80 | def test_get_rates(registry, meta_swap): 81 | assert registry.get_rates(meta_swap) == [0] * 8 82 | 83 | 84 | def test_get_coin_indices(registry, meta_swap, underlying_coins, meta_coins, n_coins, n_metacoins): 85 | for i, j in itertools.combinations(range(n_metacoins), 2): 86 | with brownie.reverts("No available market"): 87 | registry.get_coin_indices(meta_swap, meta_coins[i], meta_coins[j]) 88 | 89 | coins = meta_coins[:-1] + underlying_coins 90 | for i, j in itertools.product(range(n_coins - 1), range(n_coins, n_coins + n_metacoins - 1)): 91 | with brownie.reverts("No available market"): 92 | registry.get_coin_indices(meta_swap, coins[i], coins[j]) 93 | 94 | 95 | @pytest.mark.once 96 | def test_get_balances(registry, meta_swap): 97 | with brownie.reverts(): 98 | registry.get_balances(meta_swap) 99 | 100 | 101 | @pytest.mark.once 102 | def test_get_underlying_balances(registry, meta_swap): 103 | with brownie.reverts(): 104 | registry.get_underlying_balances(meta_swap) 105 | 106 | 107 | @pytest.mark.once 108 | def test_get_admin_balances(registry, meta_swap): 109 | with brownie.reverts(): 110 | registry.get_admin_balances(meta_swap) 111 | 112 | 113 | @pytest.mark.once 114 | def test_get_virtual_price_from_lp_token(alice, registry, meta_lp_token): 115 | with brownie.reverts(): 116 | registry.get_virtual_price_from_lp_token(meta_lp_token) 117 | 118 | 119 | @pytest.mark.once 120 | def test_get_pool_from_lp_token(registry, meta_lp_token): 121 | assert registry.get_pool_from_lp_token(meta_lp_token) == ZERO_ADDRESS 122 | 123 | 124 | @pytest.mark.once 125 | def test_get_lp_token(registry, meta_swap): 126 | assert registry.get_lp_token(meta_swap) == ZERO_ADDRESS 127 | 128 | 129 | def test_coin_count_is_correct(registry, underlying_coins): 130 | 131 | assert registry.coin_count() == len(underlying_coins) 132 | 133 | 134 | def test_get_all_swappable_coins(registry, meta_coins, underlying_coins): 135 | coin_set = set(map(str, itertools.chain(meta_coins, underlying_coins))) 136 | coin_count = len(coin_set) 137 | 138 | coins = set(registry.get_coin(i) for i in range(coin_count)) 139 | 140 | assert coins == set(map(str, underlying_coins)) | {ZERO_ADDRESS} 141 | 142 | 143 | @pytest.mark.once 144 | def test_last_updated_getter(registry, history): 145 | registry_txs = history.filter(receiver=registry.address) 146 | assert math.isclose(registry_txs[-1].timestamp, registry.last_updated()) 147 | 148 | 149 | def test_coin_swap_count(registry, meta_coins, underlying_coins): 150 | meta_coins = list(map(str, meta_coins)) 151 | underlying_coins = list(map(str, underlying_coins)) 152 | counter = Counter() 153 | 154 | underlying_pairs = itertools.chain(*itertools.combinations(underlying_coins, 2)) 155 | 156 | counter.update(underlying_pairs) 157 | 158 | for coin in meta_coins: 159 | assert registry.get_coin_swap_count(coin) == 0 160 | 161 | for coin in underlying_coins: 162 | assert registry.get_coin_swap_count(coin) == counter[coin] 163 | 164 | 165 | def test_swap_coin_for(registry, meta_coins, underlying_coins): 166 | meta_coins = list(map(str, meta_coins)) 167 | underlying_coins = list(map(str, underlying_coins)) 168 | pairings = defaultdict(set) 169 | 170 | underlying_pairs = list(itertools.combinations(map(str, underlying_coins), 2)) 171 | 172 | for coin_a, coin_b in underlying_pairs: 173 | pairings[coin_a].add(coin_b) 174 | pairings[coin_b].add(coin_a) 175 | 176 | for coin in itertools.chain(meta_coins, underlying_coins): 177 | coin_swap_count = len(pairings[coin]) 178 | available_swaps = { 179 | registry.get_coin_swap_complement(coin, i) for i in range(coin_swap_count) 180 | } 181 | 182 | assert available_swaps == pairings[coin] 183 | -------------------------------------------------------------------------------- /tests/local/unitary/Registry/test_remove_pool_no_lending.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import brownie 4 | import pytest 5 | 6 | from scripts.utils import pack_values 7 | 8 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 9 | 10 | 11 | @pytest.fixture(scope="module", autouse=True) 12 | def registry( 13 | Registry, 14 | provider, 15 | gauge_controller, 16 | alice, 17 | swap, 18 | lp_token, 19 | n_coins, 20 | is_v1, 21 | underlying_decimals, 22 | chain, 23 | ): 24 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 25 | registry.add_pool_without_underlying( 26 | swap, 27 | n_coins, 28 | lp_token, 29 | "0x00", 30 | pack_values(underlying_decimals), 31 | 0, # use rates 32 | hasattr(swap, "initial_A"), 33 | is_v1, 34 | "", 35 | {"from": alice}, 36 | ) 37 | chain.sleep(10) 38 | registry.remove_pool(swap, {"from": alice}) 39 | yield registry 40 | 41 | 42 | @pytest.mark.itercoins("send", "recv") 43 | def test_find_pool(registry, underlying_coins, send, recv): 44 | assert ( 45 | registry.find_pool_for_coins(underlying_coins[send], underlying_coins[recv]) == ZERO_ADDRESS 46 | ) 47 | 48 | 49 | def test_get_n_coins(registry, swap): 50 | assert registry.get_n_coins(swap) == [0, 0] 51 | 52 | 53 | def test_get_coins(registry, swap): 54 | assert registry.get_coins(swap) == [ZERO_ADDRESS] * 8 55 | assert registry.get_underlying_coins(swap) == [ZERO_ADDRESS] * 8 56 | 57 | 58 | def test_get_decimals(registry, swap): 59 | assert registry.get_decimals(swap) == [0] * 8 60 | assert registry.get_underlying_decimals(swap) == [0] * 8 61 | 62 | 63 | def test_get_rates(registry, swap): 64 | assert registry.get_rates(swap) == [0] * 8 65 | 66 | 67 | @pytest.mark.itercoins("send", "recv") 68 | def test_get_coin_indices(registry, swap, underlying_coins, send, recv): 69 | with brownie.reverts("No available market"): 70 | registry.get_coin_indices(swap, underlying_coins[send], underlying_coins[recv]) 71 | 72 | 73 | @pytest.mark.once 74 | def test_get_balances(registry, swap): 75 | with brownie.reverts(): 76 | registry.get_balances(swap) 77 | 78 | 79 | @pytest.mark.once 80 | def test_get_underlying_balances(registry, swap): 81 | with brownie.reverts(): 82 | registry.get_underlying_balances(swap) 83 | 84 | 85 | @pytest.mark.once 86 | def test_get_admin_balances(registry, swap): 87 | with brownie.reverts(): 88 | registry.get_admin_balances(swap) 89 | 90 | 91 | @pytest.mark.once 92 | def test_get_virtual_price_from_lp_token(alice, registry, lp_token): 93 | with brownie.reverts(): 94 | registry.get_virtual_price_from_lp_token(lp_token) 95 | 96 | 97 | @pytest.mark.once 98 | def test_get_pool_from_lp_token(registry, lp_token): 99 | assert registry.get_pool_from_lp_token(lp_token) == ZERO_ADDRESS 100 | 101 | 102 | @pytest.mark.once 103 | def test_get_lp_token(registry, swap): 104 | assert registry.get_lp_token(swap) == ZERO_ADDRESS 105 | 106 | 107 | def test_coin_count_is_correct(registry): 108 | 109 | assert registry.coin_count() == 0 110 | 111 | 112 | def test_get_all_swappable_coins(registry, underlying_coins): 113 | coin_count = len(underlying_coins) 114 | 115 | coins = set(registry.get_coin(i) for i in range(coin_count)) 116 | 117 | assert coins == {ZERO_ADDRESS} 118 | 119 | 120 | @pytest.mark.once 121 | def test_last_updated_getter(registry, history): 122 | registry_txs = history.filter(receiver=registry.address) 123 | assert math.isclose( 124 | registry_txs[-1].timestamp, registry.last_updated(), rel_tol=0.001, abs_tol=4 125 | ) 126 | 127 | 128 | def test_coin_swap_count(registry, underlying_coins): 129 | for coin in underlying_coins: 130 | assert registry.get_coin_swap_count(coin) == 0 131 | 132 | 133 | def test_swap_coin_for(registry, underlying_coins): 134 | coin_set = set(map(str, underlying_coins)) 135 | 136 | for coin in coin_set: 137 | coin_swap_count = len(coin_set) - 1 138 | swap_coins = {registry.get_coin_swap_complement(coin, i) for i in range(coin_swap_count)} 139 | 140 | assert swap_coins == {ZERO_ADDRESS} 141 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_calculator.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import ZERO_ADDRESS 4 | 5 | 6 | def test_admin_only(bob, registry_swap): 7 | with brownie.reverts("dev: admin-only function"): 8 | registry_swap.set_calculator(ZERO_ADDRESS, ZERO_ADDRESS, {"from": bob}) 9 | 10 | 11 | def test_default_calculator(alice, registry_swap, calculator): 12 | assert registry_swap.get_calculator(alice) == calculator 13 | assert registry_swap.default_calculator() == calculator 14 | 15 | 16 | def test_set_calculator(alice, bob, registry_swap): 17 | registry_swap.set_calculator(alice, bob, {"from": alice}) 18 | 19 | assert registry_swap.get_calculator(alice) == bob 20 | 21 | 22 | def test_unset_calculator(alice, bob, registry_swap): 23 | registry_swap.set_calculator(alice, bob, {"from": alice}) 24 | registry_swap.set_calculator(alice, ZERO_ADDRESS, {"from": alice}) 25 | 26 | assert registry_swap.get_calculator(alice) == registry_swap.default_calculator() 27 | 28 | 29 | def test_set_default_calculator(alice, bob, registry_swap): 30 | registry_swap.set_default_calculator(bob, {"from": alice}) 31 | 32 | assert registry_swap.get_calculator(alice) == bob 33 | assert registry_swap.default_calculator() == bob 34 | 35 | 36 | @pytest.mark.no_call_coverage 37 | def test_calculator(accounts, calculator): 38 | expected = [89743074, 100065, 37501871, 90394938, 114182] 39 | actual = calculator.get_dy.call( 40 | 2, 41 | (2241857934, 1895960155, 0, 0, 0, 0, 0, 0), 42 | 100, 43 | 4000000, 44 | (1000000000000000000, 1000000000000000000, 0, 0, 0, 0, 0, 0), 45 | (10000000000, 10000000000, 0, 0, 0, 0, 0, 0), 46 | 0, 47 | 1, 48 | [89970746, 100274, 37586976, 90624569, 114419] + [0] * 95, 49 | ) 50 | 51 | assert actual[:5] == expected 52 | 53 | 54 | @pytest.mark.no_call_coverage 55 | def test_dy_dx(accounts, calculator): 56 | dx = calculator.get_dx( 57 | 2, 58 | (2241857934, 1895960155, 0, 0, 0, 0, 0, 0), 59 | 100, 60 | 4000000, 61 | (1000000000000000000, 1000000000000000000, 0, 0, 0, 0, 0, 0), 62 | (10000000000, 10000000000, 0, 0, 0, 0, 0, 0), 63 | 0, 64 | 1, 65 | 89970746, 66 | ) 67 | assert ( 68 | calculator.get_dy( 69 | 2, 70 | (2241857934, 1895960155, 0, 0, 0, 0, 0, 0), 71 | 100, 72 | 4000000, 73 | (1000000000000000000, 1000000000000000000, 0, 0, 0, 0, 0, 0), 74 | (10000000000, 10000000000, 0, 0, 0, 0, 0, 0), 75 | 0, 76 | 1, 77 | [dx] + [0] * 99, 78 | )[0] 79 | == 89970746 80 | ) 81 | 82 | 83 | @pytest.mark.no_call_coverage 84 | def test_dx_dy(accounts, calculator): 85 | dy = calculator.get_dy( 86 | 2, 87 | (2241857934, 1895960155, 0, 0, 0, 0, 0, 0), 88 | 100, 89 | 4000000, 90 | (1000000000000000000, 1000000000000000000, 0, 0, 0, 0, 0, 0), 91 | (10000000000, 10000000000, 0, 0, 0, 0, 0, 0), 92 | 0, 93 | 1, 94 | [89970746] + [0] * 99, 95 | )[0] 96 | assert ( 97 | calculator.get_dx( 98 | 2, 99 | (2241857934, 1895960155, 0, 0, 0, 0, 0, 0), 100 | 100, 101 | 4000000, 102 | (1000000000000000000, 1000000000000000000, 0, 0, 0, 0, 0, 0), 103 | (10000000000, 10000000000, 0, 0, 0, 0, 0, 0), 104 | 0, 105 | 1, 106 | dy, 107 | ) 108 | == 89970746 109 | ) 110 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_exchange.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def registry(Registry, provider, gauge_controller, alice, swap, lp_token, n_coins, is_v1): 9 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 10 | provider.set_address(0, registry, {"from": alice}) 11 | registry.add_pool_without_underlying( 12 | swap, 13 | n_coins, 14 | lp_token, 15 | "0x00", 16 | 0, 17 | 0, # use rates 18 | hasattr(swap, "initial_A"), 19 | is_v1, 20 | "", 21 | {"from": alice}, 22 | ) 23 | yield registry 24 | 25 | 26 | @pytest.fixture(scope="module") 27 | def registry_swap(Swaps, alice, bob, registry, provider, swap, calculator, underlying_coins): 28 | contract = Swaps.deploy(provider, calculator, {"from": alice}) 29 | 30 | for coin in underlying_coins: 31 | if coin == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 32 | bob.transfer(swap, "10 ether") 33 | else: 34 | coin.approve(contract, 2**256 - 1, {"from": alice}) 35 | 36 | yield contract 37 | 38 | 39 | @pytest.mark.params(n_coins=4) 40 | @pytest.mark.itercoins("send", "recv") 41 | def test_exchange(alice, registry_swap, swap, underlying_coins, underlying_decimals, send, recv): 42 | 43 | amount = 10 ** underlying_decimals[send] 44 | 45 | send = underlying_coins[send] 46 | recv = underlying_coins[recv] 47 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 48 | value = 10**18 49 | else: 50 | send._mint_for_testing(alice, amount, {"from": alice}) 51 | value = 0 52 | 53 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 54 | eth_balance = alice.balance() 55 | 56 | expected = registry_swap.get_exchange_amount(swap, send, recv, amount) 57 | registry_swap.exchange(swap, send, recv, amount, 0, {"from": alice, "value": value}) 58 | 59 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 60 | assert registry_swap.balance() == 0 61 | else: 62 | 63 | assert send.balanceOf(registry_swap) == 0 64 | assert send.balanceOf(alice) == 0 65 | 66 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 67 | assert registry_swap.balance() == 0 68 | assert alice.balance() > eth_balance 69 | else: 70 | assert recv.balanceOf(registry_swap) == 0 71 | assert recv.balanceOf(alice) == expected 72 | 73 | 74 | @pytest.mark.params(n_coins=4) 75 | @pytest.mark.itercoins("send", "recv") 76 | def test_existing_balance( 77 | alice, bob, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 78 | ): 79 | # an existing balance within the contact should not affect the outcome of an exchange 80 | amount = 10 ** underlying_decimals[send] 81 | send = underlying_coins[send] 82 | recv = underlying_coins[recv] 83 | 84 | bob.transfer(registry_swap, "10 ether") 85 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 86 | value = 10**18 87 | else: 88 | send._mint_for_testing(alice, amount, {"from": alice}) 89 | send._mint_for_testing(registry_swap, 10**19, {"from": alice}) 90 | value = 0 91 | 92 | if recv != "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 93 | recv._mint_for_testing(registry_swap, 10**19, {"from": alice}) 94 | 95 | registry_swap.exchange(swap, send, recv, amount, 0, {"from": alice, "value": value}) 96 | 97 | assert registry_swap.balance() == "10 ether" 98 | if send != "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 99 | assert send.balanceOf(registry_swap) == 10**19 100 | if recv != "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 101 | assert recv.balanceOf(registry_swap) == 10**19 102 | 103 | 104 | @pytest.mark.params(n_coins=4) 105 | @pytest.mark.itercoins("send", "recv") 106 | def test_min_expected( 107 | alice, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 108 | ): 109 | # exchange should revert when `expected` is higher than received amount 110 | amount = 10 ** underlying_decimals[send] 111 | send = underlying_coins[send] 112 | recv = underlying_coins[recv] 113 | 114 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 115 | value = 10**18 116 | else: 117 | send._mint_for_testing(alice, amount, {"from": alice}) 118 | value = 0 119 | 120 | expected = registry_swap.get_exchange_amount(swap, send, recv, amount) 121 | with brownie.reverts(): 122 | registry_swap.exchange( 123 | swap, send, recv, amount, expected + 1, {"from": alice, "value": value} 124 | ) 125 | 126 | 127 | @pytest.mark.params(n_coins=4) 128 | @pytest.mark.itercoins("send", "recv") 129 | def test_existing_balance_insufficient_send( 130 | alice, bob, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 131 | ): 132 | # insuffucient send amount should fail an exchange, even when the contract has 133 | # an existing balance that would cover the remainder! 134 | amount = 10 ** underlying_decimals[send] 135 | send = underlying_coins[send] 136 | recv = underlying_coins[recv] 137 | 138 | bob.transfer(registry_swap, "10 ether") 139 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 140 | value = 10**18 - 1 141 | else: 142 | send._mint_for_testing(alice, amount - 1, {"from": alice}) 143 | send._mint_for_testing(registry_swap, 10**19, {"from": alice}) 144 | value = 0 145 | 146 | if recv != "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 147 | recv._mint_for_testing(registry_swap, 10**19, {"from": alice}) 148 | 149 | with brownie.reverts(): 150 | registry_swap.exchange(swap, send, recv, amount, 0, {"from": alice, "value": value}) 151 | 152 | 153 | @pytest.mark.params(n_coins=2) 154 | @pytest.mark.itercoins("send", "recv") 155 | def test_receiver( 156 | alice, bob, registry_swap, swap, underlying_coins, underlying_decimals, send, recv 157 | ): 158 | 159 | amount = 10 ** underlying_decimals[send] 160 | 161 | send = underlying_coins[send] 162 | recv = underlying_coins[recv] 163 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 164 | value = 10**18 165 | else: 166 | send._mint_for_testing(alice, amount, {"from": alice}) 167 | value = 0 168 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 169 | eth_balance = bob.balance() 170 | 171 | expected = registry_swap.get_exchange_amount(swap, send, recv, amount) 172 | registry_swap.exchange(swap, send, recv, amount, 0, bob, {"from": alice, "value": value}) 173 | 174 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 175 | assert registry_swap.balance() == 0 176 | else: 177 | 178 | assert send.balanceOf(registry_swap) == 0 179 | assert send.balanceOf(alice) == 0 180 | 181 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 182 | assert registry_swap.balance() == 0 183 | assert bob.balance() > eth_balance 184 | else: 185 | assert recv.balanceOf(registry_swap) == 0 186 | assert recv.balanceOf(alice) == 0 187 | assert recv.balanceOf(bob) == expected 188 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_exchange_lending.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | 4 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 5 | 6 | 7 | @pytest.fixture(scope="module", autouse=True) 8 | def registry( 9 | Registry, 10 | provider, 11 | gauge_controller, 12 | alice, 13 | lending_swap, 14 | lp_token, 15 | n_coins, 16 | is_v1, 17 | rate_method_id, 18 | ): 19 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 20 | provider.set_address(0, registry, {"from": alice}) 21 | registry.add_pool( 22 | lending_swap, 23 | n_coins, 24 | lp_token, 25 | rate_method_id, 26 | 0, 27 | 0, 28 | hasattr(lending_swap, "initial_A"), 29 | is_v1, 30 | "", 31 | {"from": alice}, 32 | ) 33 | yield registry 34 | 35 | 36 | @pytest.fixture(scope="module") 37 | def registry_swap( 38 | Swaps, alice, bob, registry, provider, lending_swap, calculator, underlying_coins, wrapped_coins 39 | ): 40 | contract = Swaps.deploy(provider, calculator, {"from": alice}) 41 | 42 | for underlying, wrapped in zip(underlying_coins, wrapped_coins): 43 | if underlying == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 44 | bob.transfer(lending_swap, "10 ether") 45 | continue 46 | underlying.approve(contract, 2**256 - 1, {"from": alice}) 47 | if underlying != wrapped: 48 | wrapped.approve(contract, 2**256 - 1, {"from": alice}) 49 | 50 | yield contract 51 | 52 | 53 | @pytest.mark.params(n_coins=4) 54 | @pytest.mark.itercoins("send", "recv") 55 | def test_exchange(alice, registry_swap, lending_swap, wrapped_coins, wrapped_decimals, send, recv): 56 | amount = 10 ** wrapped_decimals[send] 57 | 58 | send = wrapped_coins[send] 59 | recv = wrapped_coins[recv] 60 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 61 | value = 10**18 62 | else: 63 | send._mint_for_testing(alice, amount, {"from": alice}) 64 | value = 0 65 | 66 | expected = registry_swap.get_exchange_amount(lending_swap, send, recv, amount) 67 | registry_swap.exchange(lending_swap, send, recv, amount, 0, {"from": alice, "value": value}) 68 | 69 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 70 | assert registry_swap.balance() == 0 71 | else: 72 | 73 | assert send.balanceOf(registry_swap) == 0 74 | assert send.balanceOf(alice) == 0 75 | 76 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 77 | assert registry_swap.balance() == 0 78 | else: 79 | assert recv.balanceOf(registry_swap) == 0 80 | assert recv.balanceOf(alice) == expected 81 | 82 | 83 | @pytest.mark.params(n_coins=4, lending="cERC20") 84 | @pytest.mark.itercoins("send", "recv") 85 | def test_exchange_underlying( 86 | alice, registry_swap, lending_swap, underlying_coins, underlying_decimals, send, recv 87 | ): 88 | amount = 10 ** underlying_decimals[send] 89 | 90 | send = underlying_coins[send] 91 | recv = underlying_coins[recv] 92 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 93 | value = 10**18 94 | else: 95 | send._mint_for_testing(alice, amount, {"from": alice}) 96 | value = 0 97 | 98 | expected = registry_swap.get_exchange_amount(lending_swap, send, recv, amount) 99 | registry_swap.exchange(lending_swap, send, recv, amount, 0, {"from": alice, "value": value}) 100 | 101 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 102 | assert registry_swap.balance() == 0 103 | else: 104 | 105 | assert send.balanceOf(registry_swap) == 0 106 | assert send.balanceOf(alice) == 0 107 | 108 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 109 | assert registry_swap.balance() == 0 110 | else: 111 | assert recv.balanceOf(registry_swap) == 0 112 | assert recv.balanceOf(alice) == expected 113 | 114 | 115 | @pytest.mark.params(n_coins=4, lending="cERC20") 116 | @pytest.mark.itercoins("send", "recv") 117 | def test_min_expected( 118 | alice, registry_swap, lending_swap, underlying_coins, underlying_decimals, send, recv 119 | ): 120 | # exchange should revert when `expected` is higher than received amount 121 | amount = 10 ** underlying_decimals[send] 122 | send = underlying_coins[send] 123 | recv = underlying_coins[recv] 124 | 125 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 126 | value = 10**18 127 | else: 128 | send._mint_for_testing(alice, amount, {"from": alice}) 129 | value = 0 130 | 131 | expected = registry_swap.get_exchange_amount(lending_swap, send, recv, amount) 132 | with brownie.reverts(): 133 | registry_swap.exchange( 134 | lending_swap, send, recv, amount, expected + 1, {"from": alice, "value": value} 135 | ) 136 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_exchange_meta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 4 | 5 | 6 | @pytest.fixture(scope="module") 7 | def registry( 8 | Registry, 9 | provider, 10 | gauge_controller, 11 | alice, 12 | swap, 13 | meta_swap, 14 | lp_token, 15 | meta_lp_token, 16 | n_coins, 17 | n_metacoins, 18 | is_v1, 19 | ): 20 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 21 | provider.set_address(0, registry, {"from": alice}) 22 | registry.add_pool_without_underlying( 23 | swap, 24 | n_coins, 25 | lp_token, 26 | "0x00", 27 | 0, 28 | 0, # use rates 29 | hasattr(swap, "initial_A"), 30 | is_v1, 31 | "", 32 | {"from": alice}, 33 | ) 34 | registry.add_metapool(meta_swap, n_metacoins, meta_lp_token, 0, "", {"from": alice}) 35 | yield registry 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def registry_swap( 40 | Swaps, alice, bob, registry, provider, meta_swap, calculator, underlying_coins, meta_coins 41 | ): 42 | contract = Swaps.deploy(provider, calculator, {"from": alice}) 43 | 44 | for coin in underlying_coins: 45 | if coin == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 46 | bob.transfer(meta_swap, "10 ether") 47 | else: 48 | coin.approve(contract, 2**256 - 1, {"from": alice}) 49 | for coin in meta_coins: 50 | coin.approve(contract, 2**256 - 1, {"from": alice}) 51 | 52 | yield contract 53 | 54 | 55 | @pytest.mark.params(n_coins=4, n_metacoins=4) 56 | @pytest.mark.itermetacoins("send", "recv") 57 | def test_exchange(alice, registry_swap, meta_swap, meta_coins, meta_decimals, send, recv): 58 | amount = 10 ** meta_decimals[send] 59 | 60 | send = meta_coins[send] 61 | recv = meta_coins[recv] 62 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 63 | value = 10**18 64 | else: 65 | send._mint_for_testing(alice, amount, {"from": alice}) 66 | value = 0 67 | 68 | expected = registry_swap.get_exchange_amount(meta_swap, send, recv, amount) 69 | registry_swap.exchange(meta_swap, send, recv, amount, 0, {"from": alice, "value": value}) 70 | 71 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 72 | assert registry_swap.balance() == 0 73 | else: 74 | 75 | assert send.balanceOf(registry_swap) == 0 76 | assert send.balanceOf(alice) == 0 77 | 78 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 79 | assert registry_swap.balance() == 0 80 | else: 81 | assert recv.balanceOf(registry_swap) == 0 82 | assert recv.balanceOf(alice) == expected 83 | 84 | 85 | @pytest.mark.params(n_coins=4, n_metacoins=4) 86 | @pytest.mark.itermetacoins("send", max=2) 87 | @pytest.mark.itercoins("recv") 88 | def test_meta_to_base( 89 | alice, registry_swap, meta_swap, meta_coins, underlying_coins, meta_decimals, send, recv 90 | ): 91 | amount = 10 ** meta_decimals[send] 92 | 93 | send = meta_coins[send] 94 | recv = underlying_coins[recv] 95 | 96 | send._mint_for_testing(alice, amount, {"from": alice}) 97 | 98 | expected = registry_swap.get_exchange_amount(meta_swap, send, recv, amount) 99 | registry_swap.exchange(meta_swap, send, recv, amount, 0, {"from": alice}) 100 | 101 | assert send.balanceOf(registry_swap) == 0 102 | assert send.balanceOf(alice) == 0 103 | 104 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 105 | assert registry_swap.balance() == 0 106 | else: 107 | assert recv.balanceOf(registry_swap) == 0 108 | assert recv.balanceOf(alice) == expected 109 | 110 | 111 | @pytest.mark.params(n_coins=4, n_metacoins=4) 112 | @pytest.mark.itercoins("send") 113 | @pytest.mark.itermetacoins("recv", max=2) 114 | def test_base_to_meta( 115 | alice, registry_swap, meta_swap, meta_coins, underlying_coins, underlying_decimals, send, recv 116 | ): 117 | amount = 10 ** underlying_decimals[send] 118 | 119 | send = underlying_coins[send] 120 | recv = meta_coins[recv] 121 | 122 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 123 | value = 10**18 124 | else: 125 | value = 0 126 | send._mint_for_testing(alice, amount, {"from": alice}) 127 | 128 | expected = registry_swap.get_exchange_amount(meta_swap, send, recv, amount) 129 | registry_swap.exchange(meta_swap, send, recv, amount, 0, {"from": alice, "value": value}) 130 | 131 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 132 | assert registry_swap.balance() == 0 133 | else: 134 | assert send.balanceOf(registry_swap) == 0 135 | assert send.balanceOf(alice) == 0 136 | 137 | assert recv.balanceOf(registry_swap) == 0 138 | assert recv.balanceOf(alice) == expected 139 | 140 | 141 | @pytest.mark.params(n_coins=4, n_metacoins=4) 142 | @pytest.mark.itercoins("send", "recv") 143 | def test_exchange_underlying( 144 | alice, registry_swap, meta_swap, underlying_coins, underlying_decimals, send, recv 145 | ): 146 | amount = 10 ** underlying_decimals[send] 147 | 148 | send = underlying_coins[send] 149 | recv = underlying_coins[recv] 150 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 151 | value = 10**18 152 | else: 153 | send._mint_for_testing(alice, amount, {"from": alice}) 154 | value = 0 155 | 156 | expected = registry_swap.get_exchange_amount(meta_swap, send, recv, amount) 157 | registry_swap.exchange(meta_swap, send, recv, amount, 0, {"from": alice, "value": value}) 158 | 159 | if send == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 160 | assert registry_swap.balance() == 0 161 | else: 162 | assert send.balanceOf(registry_swap) == 0 163 | assert send.balanceOf(alice) == 0 164 | 165 | if recv == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE": 166 | assert registry_swap.balance() == 0 167 | else: 168 | assert recv.balanceOf(registry_swap) == 0 169 | assert recv.balanceOf(alice) == expected 170 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_get_best_rate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import ETH_ADDRESS, ZERO_ADDRESS 3 | from brownie.exceptions import VirtualMachineError 4 | 5 | 6 | @pytest.fixture(scope="module") 7 | def swap1(PoolMockV2, underlying_coins, alice, bob): 8 | swap = PoolMockV2.deploy(4, underlying_coins, [ZERO_ADDRESS] * 4, 70, 5000000, {"from": alice}) 9 | bob.transfer(swap, "10 ether") 10 | yield swap 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def swap2(PoolMockV2, underlying_coins, alice, bob): 15 | swap = PoolMockV2.deploy( 16 | 3, underlying_coins[1:] + [ZERO_ADDRESS], [ZERO_ADDRESS] * 4, 70, 3000000, {"from": alice} 17 | ) 18 | bob.transfer(swap, "10 ether") 19 | yield swap 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def swap3(PoolMockV2, underlying_coins, alice): 24 | yield PoolMockV2.deploy( 25 | 2, underlying_coins[:2] + [ZERO_ADDRESS] * 2, [ZERO_ADDRESS] * 4, 70, 0, {"from": alice} 26 | ) 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def registry(ERC20, Registry, provider, gauge_controller, alice, swap1, swap2, swap3, lp_token): 31 | registry = Registry.deploy(provider, gauge_controller, {"from": alice}) 32 | provider.set_address(0, registry, {"from": alice}) 33 | 34 | for swap, n_coins in ((swap1, 4), (swap2, 3), (swap3, 2)): 35 | token = ERC20.deploy("", "", 18, {"from": alice}) 36 | registry.add_pool_without_underlying( 37 | swap, 38 | n_coins, 39 | token, 40 | "0x00", 41 | 0, 42 | 0, # use rates 43 | hasattr(swap, "initial_A"), 44 | False, 45 | "", 46 | {"from": alice}, 47 | ) 48 | 49 | yield registry 50 | 51 | 52 | @pytest.fixture(scope="module") 53 | def factory(pm, alice): 54 | yield pm("curvefi/curve-factory@2.0.0").Factory.deploy({"from": alice}) 55 | 56 | 57 | @pytest.fixture(scope="module") 58 | def registry_swap(Swaps, alice, registry, provider, factory, calculator, underlying_coins): 59 | contract = Swaps.deploy(provider, calculator, {"from": alice}) 60 | provider.add_new_id(provider, "Filler", {"from": alice}) 61 | provider.add_new_id(contract, "Pool Swaps", {"from": alice}) 62 | provider.add_new_id(factory, "Factory", {"from": alice}) 63 | contract.update_registry_address({"from": alice}) 64 | 65 | for coin in underlying_coins: 66 | if coin != ETH_ADDRESS: 67 | coin.approve(contract, 2**256 - 1, {"from": alice}) 68 | coin._mint_for_testing(alice, 10**18, {"from": alice}) 69 | 70 | yield contract 71 | 72 | 73 | @pytest.mark.params(n_coins=4) 74 | @pytest.mark.itercoins("send", "recv") 75 | def test_get_best_rate(registry_swap, swap1, swap2, swap3, underlying_coins, send, recv): 76 | send = underlying_coins[send] 77 | recv = underlying_coins[recv] 78 | 79 | best_swap = None 80 | best_rate = 0 81 | for swap in (swap1, swap2, swap3): 82 | try: 83 | rate = registry_swap.get_exchange_amount(swap, send, recv, 10**18) 84 | if rate > best_rate: 85 | best_rate = rate 86 | best_swap = swap 87 | except VirtualMachineError: 88 | pass 89 | 90 | assert registry_swap.get_best_rate(send, recv, 10**18) == (best_swap, best_rate) 91 | 92 | 93 | @pytest.mark.params(n_coins=4) 94 | @pytest.mark.itercoins("send", "recv") 95 | def test_get_best_rate_with_exclusion( 96 | registry_swap, swap1, swap2, swap3, underlying_coins, send, recv 97 | ): 98 | send = underlying_coins[send] 99 | recv = underlying_coins[recv] 100 | exclude_list = [swap1] + [ZERO_ADDRESS] * 7 101 | 102 | best_swap = ZERO_ADDRESS 103 | best_rate = 0 104 | for swap in (swap1, swap2, swap3): 105 | try: 106 | rate = registry_swap.get_exchange_amount(swap, send, recv, 10**18) 107 | if rate > best_rate and swap not in exclude_list: 108 | best_rate = rate 109 | best_swap = swap 110 | except VirtualMachineError: 111 | pass 112 | 113 | assert registry_swap.get_best_rate(send, recv, 10**18, exclude_list) == (best_swap, best_rate) 114 | 115 | 116 | @pytest.mark.params(n_coins=4) 117 | @pytest.mark.itercoins("send", "recv") 118 | def test_exchange_with_best_rate( 119 | alice, registry_swap, swap1, swap2, swap3, underlying_coins, underlying_decimals, send, recv 120 | ): 121 | amount = 10 ** underlying_decimals[send] 122 | 123 | send = underlying_coins[send] 124 | recv = underlying_coins[recv] 125 | 126 | best_swap = None 127 | best_rate = 0 128 | for swap in (swap1, swap2, swap3): 129 | try: 130 | rate = registry_swap.get_exchange_amount(swap, send, recv, amount) 131 | if rate > best_rate: 132 | best_rate = rate 133 | best_swap = swap 134 | except VirtualMachineError: 135 | pass 136 | 137 | value = 10**18 if send == ETH_ADDRESS else 0 138 | tx = registry_swap.exchange_with_best_rate( 139 | send, recv, amount, 0, {"from": alice, "value": value} 140 | ) 141 | 142 | assert tx.events["TokenExchange"]["pool"] == best_swap 143 | -------------------------------------------------------------------------------- /tests/local/unitary/Swaps/test_token_balance.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | 3 | 4 | def test_admin_only(registry_swap, bob, lp_token): 5 | with brownie.reverts("dev: admin-only function"): 6 | registry_swap.claim_balance(lp_token, {"from": bob}) 7 | 8 | 9 | def test_claim_normal(registry_swap, alice, lp_token): 10 | lp_token._mint_for_testing(registry_swap, 10**18, {"from": alice}) 11 | registry_swap.claim_balance(lp_token, {"from": alice}) 12 | 13 | assert lp_token.balanceOf(registry_swap) == 0 14 | assert lp_token.balanceOf(alice) == 10**18 15 | 16 | 17 | def test_claim_no_return(registry_swap, alice, ERC20NoReturn): 18 | token = ERC20NoReturn.deploy("Test", "TST", 18, {"from": alice}) 19 | token._mint_for_testing(registry_swap, 10**18, {"from": alice}) 20 | registry_swap.claim_balance(token, {"from": alice}) 21 | 22 | assert token.balanceOf(registry_swap) == 0 23 | assert token.balanceOf(alice) == 10**18 24 | 25 | 26 | def test_claim_return_false(registry_swap, alice, ERC20ReturnFalse): 27 | token = ERC20ReturnFalse.deploy("Test", "TST", 18, {"from": alice}) 28 | token._mint_for_testing(registry_swap, 10**18, {"from": alice}) 29 | registry_swap.claim_balance(token, {"from": alice}) 30 | 31 | assert token.balanceOf(registry_swap) == 0 32 | assert token.balanceOf(alice) == 10**18 33 | 34 | 35 | def test_claim_ether(registry_swap, alice, bob): 36 | bob.transfer(registry_swap, "1 ether") 37 | balance = alice.balance() 38 | 39 | registry_swap.claim_balance("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", {"from": alice}) 40 | assert alice.balance() == balance + "1 ether" 41 | --------------------------------------------------------------------------------