diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..4d2ab9fa --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +plugins = boa.coverage diff --git a/.env-example b/.env-example index 4c045d3c..d23c149e 100644 --- a/.env-example +++ b/.env-example @@ -1,2 +1,2 @@ WEB3_ALCHEMY_API_KEY= -ETHERSCAN_API_KEY= \ No newline at end of file +ETHERSCAN_API_KEY= diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 304adc19..00000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 160 -ignore = E731,E126,E121,E123,E221,E114,E116,E402,E226,W503,E501 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..e0f331dd --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +05e812182aa1267beca1b619d5882f4eb917e423 diff --git a/.github/workflows/act.sh b/.github/workflows/act.sh new file mode 100755 index 00000000..d89f8dc3 --- /dev/null +++ b/.github/workflows/act.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +# Check if the user provided a YAML file as an argument +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +WORKFLOW_FILE="$1" + +# Run the act command with the provided workflow file +act -W "$WORKFLOW_FILE" \ + --container-architecture linux/amd64 \ + -P ubuntu-latest=catthehacker/ubuntu:act-latest \ + --pull=false \ diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..493f7996 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,27 @@ +name: pre-commit + +on: [push] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install Python 3.12.6 + run: uv python install 3.12.6 + + - name: Run pre-commit checks + run: uvx pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..d02676c1 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,56 @@ +name: tests-boa + +on: [push] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HYPOTHESIS_PROFILE: no-shrink + +jobs: + tests: + name: ${{ matrix.folder }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + folder: + - "tests/unitary" + - "tests/e2e" + # - "tests/stateful" not ready yet + # Old tests to be migrated + - "tests/flashloan" + - "tests/lm_callback" + - "tests/controller" + - "tests/price_oracles" + - "tests/zaps" + - "tests/amm" + - "tests/lending" + - "tests/stableborrow --ignore=tests/stableborrow/stabilize" + - "tests/stableborrow/stabilize" + steps: + - name: Checkout repo + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install uv + uses: astral-sh/setup-uv@v6.7 + with: + enable-cache: true # Enables built-in caching for uv + + - name: Cache Compiler Installations + uses: actions/cache@v3 + with: + path: | + ~/.vvm + key: compiler-cache-${{ hashFiles('**/uv.lock') }} + + - name: Set up Python 3.12.6 + run: uv python install 3.12.6 + + - name: Install Requirements + run: uv sync + + - name: Run tests + run: | + uv run pytest ${{ matrix.folder }} -n auto diff --git a/.gitignore b/.gitignore index 99316a81..294eb2a9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,17 @@ venv /hardhat.config.js .idea -.python-version .env /.env-* /.py312 /scripts/networks.py /brownie-deploy-emas/build +.coverage +.claude +.vscode +.venv +prof +.prof +.pytest_cache +AGENTS.md +.ruff_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..441b9be2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: check-ast + - id: detect-private-key + + - repo: https://github.com/kynan/nbstripout + rev: 0.8.1 + hooks: + - id: nbstripout + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.0 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + # - repo: https://github.com/AlbertoCentonze/natrix + # rev: v0.1.9 # Use the latest version + # hooks: + # - id: natrix + # args: [lint, contracts/lending/LendingFactory.vy] # TODO slowly increase natrix coverage + + - repo: https://github.com/benber86/mamushi + rev: main + hooks: + - id: mamushi + args: [--line-length=100] + files: ^(contracts/lending/LendingFactory\.vy|contracts/zaps/CreateFromPool\.vy)$ # TODO slowly increase natrix coverage diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/ape-config.yaml b/ape-config.yaml deleted file mode 100644 index 7dd9c22d..00000000 --- a/ape-config.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: curve-stablecoin - -plugins: - - name: vyper - - name: alchemy - - name: hardhat - - name: ledger - - name: etherscan - -ethereum: - default_network: mainnet-fork - mainnet_fork: - default_provider: hardhat - transaction_acceptance_timeout: 99999999 - mainnet: - transaction_acceptance_timeout: 99999999 - -hardhat: - port: auto - fork: - ethereum: - mainnet: - upstream_provider: alchemy - enable_hardhat_deployments: true - -geth: - ethereum: - mainnet: - uri: http://localhost:9090 - -test: - mnemonic: test test test test test test test test test test test junk - number_of_accounts: 5 diff --git a/ape-run.sh b/ape-run.sh deleted file mode 100755 index 921cf86c..00000000 --- a/ape-run.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Check if arguments are provided -if [ -z "$1" ] || [ -z "$2" ]; then - echo "Usage: ./ape-run.sh " - exit 1 -fi - -COMMAND=$1 -NETWORK_TYPE=$2 - -# Run the appropriate command based on the provided argument -case $1 in - test) - ape test --network ethereum:mainnet-fork:hardhat tests_forked - ;; - clean-registry) - ape run scripts/setup-metaregistry.py clean --network ethereum:"$NETWORK_TYPE" - ;; - setup-registry) - ape run scripts/setup-metaregistry.py setup --network ethereum:"$NETWORK_TYPE" - ;; - *) - echo "Invalid argument. Please use 'test' or 'deploy'" - exit 1 - ;; -esac diff --git a/brownie-config.yaml b/brownie-config.yaml deleted file mode 100644 index 023acfc3..00000000 --- a/brownie-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -networks: - development: - cmd_settings: - default_balance: 1000000 - chain_id: 1337 diff --git a/brownie-deploy-emas/scripts/deploy_emas.py b/brownie-deploy-emas/scripts/deploy_emas.py index 6ea9616e..0fa18640 100644 --- a/brownie-deploy-emas/scripts/deploy_emas.py +++ b/brownie-deploy-emas/scripts/deploy_emas.py @@ -22,30 +22,32 @@ INTERVAL = 30 FEEDS = { - 'optimism-main': [ - ('ETH', '0x13e3Ee699D1909E989722E753853AE30b17e08c5'), - ('wstETH', '0x698B585CbC4407e2D54aa898B2600B53C68958f7'), - ('WBTC', '0x718A5788b89454aAE3A028AE9c111A29Be6c2a6F'), - ('OP', '0x0D276FC14719f9292D5C1eA2198673d1f4269246'), - ('CRV', '0xbD92C6c284271c227a1e0bF1786F468b539f51D9'), - ('VELO', '0x0f2Ed59657e391746C1a097BDa98F2aBb94b1120'), + "optimism-main": [ + ("ETH", "0x13e3Ee699D1909E989722E753853AE30b17e08c5"), + ("wstETH", "0x698B585CbC4407e2D54aa898B2600B53C68958f7"), + ("WBTC", "0x718A5788b89454aAE3A028AE9c111A29Be6c2a6F"), + ("OP", "0x0D276FC14719f9292D5C1eA2198673d1f4269246"), + ("CRV", "0xbD92C6c284271c227a1e0bF1786F468b539f51D9"), + ("VELO", "0x0f2Ed59657e391746C1a097BDa98F2aBb94b1120"), + ], + "fraxtal-main": [ + ("ETH", "0x89e60b56efD70a1D4FBBaE947bC33cae41e37A72"), + ("FRAX", "0xa41107f9259bB835275eaCaAd8048307B80D7c00"), + ("FXS", "0xbf228a9131AB3BB8ca8C7a4Ad574932253D99Cd1"), + ("CRV", "0x6C5090e85a65038ca6AB207CDb9e7a897cb33e4d"), ], - 'fraxtal-main': [ - ('ETH', '0x89e60b56efD70a1D4FBBaE947bC33cae41e37A72'), - ('FRAX', '0xa41107f9259bB835275eaCaAd8048307B80D7c00'), - ('FXS', '0xbf228a9131AB3BB8ca8C7a4Ad574932253D99Cd1'), - ('CRV', '0x6C5090e85a65038ca6AB207CDb9e7a897cb33e4d'), - ] } def main(): - babe = accounts.load('babe') + babe = accounts.load("babe") current_network = network.show_active() feed_list = FEEDS[current_network] - args = {'from': babe, 'priority_fee': 'auto', 'required_confs': 5} - print(f'Deploying on {current_network}') + args = {"from": babe, "priority_fee": "auto", "required_confs": 5} + print(f"Deploying on {current_network}") for name, feed in feed_list: - oracle = ChainlinkEMA.deploy(feed, OBSERVATIONS, INTERVAL, args, publish_source=False) - print(f'{name}: {oracle.address} - {oracle.price() / 1e18}') + oracle = ChainlinkEMA.deploy( + feed, OBSERVATIONS, INTERVAL, args, publish_source=False + ) + print(f"{name}: {oracle.address} - {oracle.price() / 1e18}") diff --git a/contracts/AMM.vy b/contracts/AMM.vy index a627f420..6349c6cf 100644 --- a/contracts/AMM.vy +++ b/contracts/AMM.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 """ @title LLAMMA - crvUSD AMM @author Curve.Fi @@ -34,72 +34,33 @@ # (f + x) * (g + y) = Inv = p_oracle * A**2 * y0**2 # ======================= -interface ERC20: - def transfer(_to: address, _value: uint256) -> bool: nonpayable - def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable - def approve(_spender: address, _value: uint256) -> bool: nonpayable +from contracts.interfaces import IAMM +implements: IAMM -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable +from contracts.interfaces import IPriceOracle +from contracts.interfaces import ILMGauge +from curve_std.interfaces import IERC20 -interface LMGauge: - def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT], size: uint256): nonpayable - def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT], size: uint256): nonpayable +from snekmate.utils import math +from contracts import constants as c -event TokenExchange: - buyer: indexed(address) - sold_id: uint256 - tokens_sold: uint256 - bought_id: uint256 - tokens_bought: uint256 +from curve_std import token as tkn -event Deposit: - provider: indexed(address) - amount: uint256 - n1: int256 - n2: int256 -event Withdraw: - provider: indexed(address) - amount_borrowed: uint256 - amount_collateral: uint256 - -event SetRate: - rate: uint256 - rate_mul: uint256 - time: uint256 - -event SetFee: - fee: uint256 - -event SetAdminFee: - fee: uint256 - - -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 +# TODO common constants +# https://github.com/vyperlang/vyper/issues/4723 +WAD: constant(uint256) = c.WAD +MAX_TICKS: constant(int256) = c.MAX_TICKS +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES MAX_SKIP_TICKS: constant(int256) = 1024 +MAX_SKIP_TICKS_UINT: constant(uint256) = 1024 -struct UserTicks: - ns: int256 # packs n1 and n2, each is int128 - ticks: uint256[MAX_TICKS/2] # Share fractions packed 2 per slot - -struct DetailedTrade: - in_amount: uint256 - out_amount: uint256 - n1: int256 - n2: int256 - ticks_in: DynArray[uint256, MAX_TICKS_UINT] - last_tick_j: uint256 - admin_fee: uint256 - - -BORROWED_TOKEN: immutable(ERC20) # x +BORROWED_TOKEN: immutable(IERC20) # x BORROWED_PRECISION: immutable(uint256) -COLLATERAL_TOKEN: immutable(ERC20) # y +COLLATERAL_TOKEN: immutable(IERC20) # y COLLATERAL_PRECISION: immutable(uint256) BASE_PRICE: immutable(uint256) admin: public(address) @@ -113,7 +74,6 @@ LOG_A_RATIO: immutable(int256) # ln(A / (A - 1)) MAX_ORACLE_DN_POW: immutable(uint256) # (A / (A - 1)) ** 50 fee: public(uint256) -admin_fee: public(uint256) rate: public(uint256) rate_time: uint256 rate_mul: uint256 @@ -121,10 +81,15 @@ active_band: public(int256) min_band: public(int256) max_band: public(int256) -admin_fees_x: public(uint256) -admin_fees_y: public(uint256) +_price_oracle: IPriceOracle + +# https://github.com/vyperlang/vyper/issues/4721 +@view +@external +def price_oracle_contract() -> IPriceOracle: + return self._price_oracle + -price_oracle_contract: public(immutable(PriceOracle)) old_p_o: uint256 old_dfee: uint256 prev_p_o_time: uint256 @@ -135,25 +100,31 @@ bands_x: public(HashMap[int256, uint256]) bands_y: public(HashMap[int256, uint256]) total_shares: HashMap[int256, uint256] -user_shares: public(HashMap[address, UserTicks]) -DEAD_SHARES: constant(uint256) = 1000 +user_shares: public(HashMap[address, IAMM.UserTicks]) -liquidity_mining_callback: public(LMGauge) +_liquidity_mining_callback: ILMGauge +# https://github.com/vyperlang/vyper/issues/4721 +@view @external +def liquidity_mining_callback() -> ILMGauge: + return self._liquidity_mining_callback + + +@deploy def __init__( - _borrowed_token: address, + _borrowed_token: IERC20, _borrowed_precision: uint256, - _collateral_token: address, + _collateral_token: IERC20, _collateral_precision: uint256, _A: uint256, _sqrt_band_ratio: uint256, _log_A_ratio: int256, _base_price: uint256, - fee: uint256, - admin_fee: uint256, - _price_oracle_contract: address, + _fee: uint256, + _admin_fee: uint256, + _price_oracle: IPriceOracle, ): """ @notice LLAMMA constructor @@ -164,14 +135,14 @@ def __init__( @param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18) @param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18) @param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0 - @param fee Relative fee of the AMM: int(fee * 1e18) - @param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18) - @param _price_oracle_contract External price oracle which has price() and price_w() methods + @param _fee Relative fee of the AMM: int(fee * 1e18) + @param _admin_fee DEPRECATED, left for backward compatibility + @param _price_oracle External price oracle which has price() and price_w() methods which both return current price of collateral multiplied by 1e18 """ - BORROWED_TOKEN = ERC20(_borrowed_token) + BORROWED_TOKEN = _borrowed_token BORROWED_PRECISION = _borrowed_precision - COLLATERAL_TOKEN = ERC20(_collateral_token) + COLLATERAL_TOKEN = _collateral_token COLLATERAL_PRECISION = _collateral_precision A = _A BASE_PRICE = _base_price @@ -180,11 +151,10 @@ def __init__( A2 = pow_mod256(A, 2) Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) - self.fee = fee - self.admin_fee = admin_fee - price_oracle_contract = PriceOracle(_price_oracle_contract) + self.fee = _fee + self._price_oracle = _price_oracle self.prev_p_o_time = block.timestamp - self.old_p_o = price_oracle_contract.price() + self.old_p_o = staticcall self._price_oracle.price() self.rate_mul = 10**18 @@ -196,20 +166,11 @@ def __init__( # (A / (A - 1)) ** 50 # This is not gas-optimal but good with bytecode size and does not overflow pow: uint256 = 10**18 - for i in range(50): + for i: uint256 in range(50): pow = unsafe_div(pow * A, Aminus1) MAX_ORACLE_DN_POW = pow -@internal -def approve_max(token: ERC20, _admin: address): - """ - Approve max in a separate function because it uses less bytespace than - calling directly, and gas doesn't matter in set_admin - """ - assert token.approve(_admin, max_value(uint256), default_return_value=True) - - @external def set_admin(_admin: address): """ @@ -218,8 +179,8 @@ def set_admin(_admin: address): """ assert self.admin == empty(address) self.admin = _admin - self.approve_max(BORROWED_TOKEN, _admin) - self.approve_max(COLLATERAL_TOKEN, _admin) + tkn.max_approve(BORROWED_TOKEN, _admin) + tkn.max_approve(COLLATERAL_TOKEN, _admin) @internal @@ -233,7 +194,7 @@ def sqrt_int(_x: uint256) -> uint256: @external -@pure +@view def coins(i: uint256) -> address: return [BORROWED_TOKEN.address, COLLATERAL_TOKEN.address][i] @@ -266,14 +227,14 @@ def limit_p_o(p: uint256) -> uint256[2]: # ratio = p_o_min / p_o_max if p > old_p_o: ratio = unsafe_div(old_p_o * 10**18, p) - if ratio < 10**36 / MAX_P_O_CHG: + if ratio < 10**36 // MAX_P_O_CHG: p_new = unsafe_div(old_p_o * MAX_P_O_CHG, 10**18) - ratio = 10**36 / MAX_P_O_CHG + ratio = 10**36 // MAX_P_O_CHG else: ratio = unsafe_div(p * 10**18, old_p_o) - if ratio < 10**36 / MAX_P_O_CHG: + if ratio < 10**36 // MAX_P_O_CHG: p_new = unsafe_div(old_p_o * 10**18, MAX_P_O_CHG) - ratio = 10**36 / MAX_P_O_CHG + ratio = 10**36 // MAX_P_O_CHG # ratio is lower than 1e18 # Also guaranteed to be limited, therefore can have all ops unsafe @@ -289,7 +250,7 @@ def limit_p_o(p: uint256) -> uint256[2]: @internal -@pure +@view def get_dynamic_fee(p_o: uint256, p_o_up: uint256) -> uint256: """ Dynamic fee equal to a quarter of difference between current price and the price of price oracle @@ -297,9 +258,9 @@ def get_dynamic_fee(p_o: uint256, p_o_up: uint256) -> uint256: p_c_d: uint256 = unsafe_div(unsafe_div(p_o ** 2, p_o_up) * p_o, p_o_up) p_c_u: uint256 = unsafe_div(unsafe_div(p_c_d * A, Aminus1) * A, Aminus1) if p_o < p_c_d: - return unsafe_div(unsafe_sub(p_c_d, p_o) * (10**18 / 4), p_c_d) + return unsafe_div(unsafe_sub(p_c_d, p_o) * (10**18 // 4), p_c_d) elif p_o > p_c_u: - return unsafe_div(unsafe_sub(p_o, p_c_u) * (10**18 / 4), p_o) + return unsafe_div(unsafe_sub(p_o, p_c_u) * (10**18 // 4), p_o) else: return 0 @@ -307,12 +268,12 @@ def get_dynamic_fee(p_o: uint256, p_o_up: uint256) -> uint256: @internal @view def _price_oracle_ro() -> uint256[2]: - return self.limit_p_o(price_oracle_contract.price()) + return self.limit_p_o(staticcall self._price_oracle.price()) @internal def _price_oracle_w() -> uint256[2]: - p: uint256[2] = self.limit_p_o(price_oracle_contract.price_w()) + p: uint256[2] = self.limit_p_o(extcall self._price_oracle.price_w()) self.prev_p_o_time = block.timestamp self.old_p_o = p[0] self.old_dfee = p[1] @@ -392,38 +353,11 @@ def _p_oracle_up(n: int256) -> uint256: power: int256 = -n * LOG_A_RATIO # ((A - 1) / A) ** n = exp(-n * ln(A / (A - 1))) = exp(-n * LOG_A_RATIO) - ## Exp implementation based on solmate's - assert power > -41446531673892821376 - assert power < 135305999368893231589 - - x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18) - - k: int256 = unsafe_div( - unsafe_add( - unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128), - 2**95), - 2**96) - x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128)) - - y: int256 = unsafe_add(x, 1346386616545796478920950773328) - y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442) - p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812) - p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240) - p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96)) - - q: int256 = x - 2855989394907223263936484059900 - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023) - - exp_result: uint256 = shift( - unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667), - unsafe_sub(k, 195)) - ## End exp + exp_result: uint256 = convert(math._wad_exp(power), uint256) + assert exp_result > 1000 # dev: limit precision of the multiplier - return unsafe_div(self._base_price() * exp_result, 10**18) + # TODO use WAD constant here + return unsafe_div(self._base_price() * exp_result, WAD) @internal @@ -440,7 +374,7 @@ def _p_current_band(n: int256) -> uint256: # return self.p_oracle**3 / p_base**2 p_oracle: uint256 = self._price_oracle_ro()[0] - return unsafe_div(p_oracle**2 / p_base * p_oracle, p_base) + return unsafe_div(p_oracle**2 // p_base * p_oracle, p_base) @external @@ -488,13 +422,13 @@ def p_oracle_down(n: int256) -> uint256: @internal -@pure +@view def _get_y0(x: uint256, y: uint256, p_o: uint256, p_o_up: uint256) -> uint256: """ @notice Calculate y0 for the invariant based on current liquidity in band. - The value of y0 has a meaning of amount of collateral when band has no stablecoin + The value of y0 has a meaning of amount of collateral when band has no borrowed tokens but current price is equal to both oracle price and upper band price. - @param x Amount of stablecoin in band + @param x Amount of borrowed in band @param y Amount of collateral in band @param p_o External oracle price @param p_o_up Upper boundary of the band @@ -508,7 +442,7 @@ def _get_y0(x: uint256, y: uint256, p_o: uint256, p_o_up: uint256) -> uint256: if x != 0: b = unsafe_div(p_o_up * Aminus1 * x, p_o) if y != 0: - b += unsafe_div(A * p_o**2 / p_o_up * y, 10**18) + b += unsafe_div(A * p_o**2 // p_o_up * y, 10**18) if x > 0 and y > 0: D: uint256 = b**2 + unsafe_div((unsafe_mul(4, A) * p_o) * y, 10**18) * x return unsafe_div((b + self.sqrt_int(D)) * 10**18, unsafe_mul(unsafe_mul(2, A), p_o)) @@ -522,7 +456,7 @@ def _get_p(n: int256, x: uint256, y: uint256) -> uint256: """ @notice Get current AMM price in band @param n Band number - @param x Amount of stablecoin in band + @param x Amount of borrowed in band @param y Amount of collateral in band @return Current price at 1e18 base """ @@ -539,7 +473,7 @@ def _get_p(n: int256, x: uint256, y: uint256) -> uint256: return unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) if y == 0: # Highest point of this band -> p_current_up p_o_up = unsafe_div(p_o_up * Aminus1, A) # now this is _actually_ p_o_down - return unsafe_div(p_o**2 / p_o_up * p_o, p_o_up) + return unsafe_div(p_o**2 // p_o_up * p_o, p_o_up) y0: uint256 = self._get_y0(x, y, p_o, p_o_up) # ^ that call also checks that p_o != 0 @@ -547,12 +481,12 @@ def _get_p(n: int256, x: uint256, y: uint256) -> uint256: # (f(y0) + x) / (g(y0) + y) f: uint256 = unsafe_div(A * y0 * p_o, p_o_up) * p_o g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o) - return (f + x * 10**18) / (g + y) + return (f + x * 10**18) // (g + y) @external @view -@nonreentrant('lock') +@nonreentrant def get_p() -> uint256: """ @notice Get current AMM price in active_band @@ -581,7 +515,7 @@ def _read_user_tick_numbers(user: address) -> int256[2]: @external @view -@nonreentrant('lock') +@nonreentrant def read_user_tick_numbers(user: address) -> int256[2]: """ @notice Unpacks and reads user tick numbers @@ -602,26 +536,26 @@ def _read_user_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICK """ ticks: DynArray[uint256, MAX_TICKS_UINT] = [] size: uint256 = convert(ns[1] - ns[0] + 1, uint256) - for i in range(MAX_TICKS / 2): + for i: uint256 in range(MAX_TICKS_UINT // 2): if len(ticks) == size: break tick: uint256 = self.user_shares[user].ticks[i] ticks.append(tick & (2**128 - 1)) if len(ticks) == size: break - ticks.append(shift(tick, -128)) + ticks.append(tick >> 128) return ticks @external @view -@nonreentrant('lock') +@nonreentrant def can_skip_bands(n_end: int256) -> bool: """ @notice Check that we have no liquidity between active_band and `n_end` """ n: int256 = self.active_band - for i in range(MAX_SKIP_TICKS): + for i: uint256 in range(MAX_SKIP_TICKS_UINT): if n_end > n: if self.bands_y[n] != 0: return False @@ -642,12 +576,12 @@ def can_skip_bands(n_end: int256) -> bool: @external @view -@nonreentrant('lock') +@nonreentrant def active_band_with_skip() -> int256: n0: int256 = self.active_band n: int256 = n0 min_band: int256 = self.min_band - for i in range(MAX_SKIP_TICKS): + for i: uint256 in range(MAX_SKIP_TICKS_UINT): if n < min_band: n = n0 - MAX_SKIP_TICKS break @@ -659,7 +593,7 @@ def active_band_with_skip() -> int256: @external @view -@nonreentrant('lock') +@nonreentrant def has_liquidity(user: address) -> bool: """ @notice Check if `user` has any liquidity in the AMM @@ -670,19 +604,19 @@ def has_liquidity(user: address) -> bool: @internal def save_user_shares(user: address, user_shares: DynArray[uint256, MAX_TICKS_UINT]): ptr: uint256 = 0 - for j in range(MAX_TICKS_UINT / 2): + for j: uint256 in range(MAX_TICKS_UINT // 2): if ptr >= len(user_shares): break tick: uint256 = user_shares[ptr] ptr = unsafe_add(ptr, 1) if len(user_shares) != ptr: - tick = tick | shift(user_shares[ptr], 128) + tick = tick | (user_shares[ptr] << 128) ptr = unsafe_add(ptr, 1) self.user_shares[user].ticks[j] = tick @external -@nonreentrant('lock') +@nonreentrant def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): """ @notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it @@ -711,18 +645,18 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128)) - lm: LMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback # Autoskip bands if we can - for i in range(MAX_SKIP_TICKS + 1): + for i: uint256 in range(MAX_SKIP_TICKS_UINT + 1): if n1 > n0: if i != 0: self.active_band = n0 break - assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band" + assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS_UINT, "Deposit below current band" n0 -= 1 - for i in range(MAX_TICKS): + for i: int256 in range(MAX_TICKS): band: int256 = unsafe_add(n1, i) if band > n2: break @@ -755,21 +689,21 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): self.save_user_shares(user, user_shares) - log Deposit(user, amount, n1, n2) + log IAMM.Deposit(provider=user, amount=amount, n1=n1, n2=n2) if lm.address != empty(address): success: bool = False res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - _abi_encode( + abi_encode( n1, collateral_shares, n_bands, method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") ), max_outsize=32, revert_on_failure=False) success, res = raw_call( lm.address, - _abi_encode( + abi_encode( user, n1, empty(DynArray[uint256, MAX_TICKS_UINT]), n_bands, method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") ), @@ -777,18 +711,18 @@ def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): @external -@nonreentrant('lock') +@nonreentrant def withdraw(user: address, frac: uint256) -> uint256[2]: """ @notice Withdraw liquidity for the user. Only admin contract can do it @param user User who owns liquidity @param frac Fraction to withdraw (1e18 being 100%) - @return Amount of [stablecoins, collateral] withdrawn + @return Amount of [borrowed, collateral] withdrawn """ assert msg.sender == self.admin assert frac <= 10**18 - lm: LMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback ns: int256[2] = self._read_user_tick_numbers(user) n: int256 = ns[0] @@ -803,7 +737,7 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: old_max_band: int256 = self.max_band max_band: int256 = n - 1 - for i in range(MAX_TICKS): + for i: uint256 in range(MAX_TICKS_UINT): x: uint256 = self.bands_x[n] y: uint256 = self.bands_y[n] ds: uint256 = unsafe_div(frac * user_shares[i], 10**18) @@ -818,12 +752,8 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: x -= dx y -= dy - # If withdrawal is the last one - transfer dust to admin fees + # If withdrawal is the last one - leave dust in the AMM if new_shares == 0: - if x > 0: - self.admin_fees_x += unsafe_div(x, BORROWED_PRECISION) - if y > 0: - self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION) x = 0 y = 0 @@ -856,21 +786,21 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: total_x = unsafe_div(total_x, BORROWED_PRECISION) total_y = unsafe_div(total_y, COLLATERAL_PRECISION) - log Withdraw(user, total_x, total_y) + log IAMM.Withdraw(provider=user, amount_borrowed=total_x, amount_collateral=total_y) if lm.address != empty(address): success: bool = False res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - _abi_encode( - ns[0], empty(DynArray[uint256, MAX_TICKS_UINT]), len(old_user_shares), # collateral/shares ratio is unchanged + abi_encode( + ns[0], empty(DynArray[uint256, MAX_TICKS_UINT]), len(old_user_shares), method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") ), max_outsize=32, revert_on_failure=False) success, res = raw_call( lm.address, - _abi_encode( + abi_encode( user, ns[0], old_user_shares, len(old_user_shares), method_id=method_id("callback_user_shares(address,int256,uint256[],uint256)") ), @@ -881,7 +811,7 @@ def withdraw(user: address, frac: uint256) -> uint256[2]: @internal @view -def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade: +def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> IAMM.DetailedTrade: """ @notice Calculate the amount which can be obtained as a result of exchange. If couldn't exchange all - will also update the amount which was actually used. @@ -898,7 +828,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: # pump = False: collateral (ETH) in, borrowable (USD) out; going down min_band: int256 = self.min_band max_band: int256 = self.max_band - out: DetailedTrade = empty(DetailedTrade) + out: IAMM.DetailedTrade = empty(IAMM.DetailedTrade) out.n2 = self.active_band p_o_up: uint256 = self._p_oracle_up(out.n2) x: uint256 = self.bands_x[out.n2] @@ -906,10 +836,9 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: in_amount_left: uint256 = in_amount fee: uint256 = max(self.fee, p_o[1]) - admin_fee: uint256 = self.admin_fee j: uint256 = MAX_TICKS_UINT - for i in range(MAX_TICKS + MAX_SKIP_TICKS): + for i: uint256 in range(MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT): y0: uint256 = 0 f: uint256 = 0 g: uint256 = 0 @@ -921,7 +850,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: out.n1 = out.n2 j = 0 y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o - f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18) + f = unsafe_div(A * y0 * p_o[0] // p_o_up * p_o[0], 10**18) g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0]) Inv = (f + x) * (g + y) dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee) @@ -949,27 +878,23 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: if dx >= in_amount_left: # This is the last band x_dest = unsafe_div(in_amount_left * 10**18, antifee) # LESS than in_amount_left - out.last_tick_j = min(Inv / (f + (x + x_dest)) - g + 1, y) # Should be always >= 0 - x_dest = unsafe_div(unsafe_sub(in_amount_left, x_dest) * admin_fee, 10**18) # abs admin fee now + out.last_tick_j = min(Inv // (f + (x + x_dest)) - g + 1, y) # Should be always >= 0 x += in_amount_left # x is precise after this # Round down the output out.out_amount += y - out.last_tick_j - out.ticks_in[j] = x - x_dest + out.ticks_in[j] = x out.in_amount = in_amount - out.admin_fee = unsafe_add(out.admin_fee, x_dest) break else: # We go into the next band dx = max(dx, 1) # Prevents from leaving dust in the band - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now in_amount_left -= dx - out.ticks_in[j] = x + dx - x_dest + out.ticks_in[j] = x + dx out.in_amount += dx out.out_amount += y - out.admin_fee = unsafe_add(out.admin_fee, x_dest) - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: + if i != MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT - 1: if out.n2 == max_band: break if j == MAX_TICKS_UINT - 1: @@ -990,26 +915,22 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: if dy >= in_amount_left: # This is the last band y_dest = unsafe_div(in_amount_left * 10**18, antifee) - out.last_tick_j = min(Inv / (g + (y + y_dest)) - f + 1, x) - y_dest = unsafe_div(unsafe_sub(in_amount_left, y_dest) * admin_fee, 10**18) # abs admin fee now + out.last_tick_j = min(Inv // (g + (y + y_dest)) - f + 1, x) y += in_amount_left out.out_amount += x - out.last_tick_j - out.ticks_in[j] = y - y_dest + out.ticks_in[j] = y out.in_amount = in_amount - out.admin_fee = unsafe_add(out.admin_fee, y_dest) break else: # We go into the next band dy = max(dy, 1) # Prevents from leaving dust in the band - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now in_amount_left -= dy - out.ticks_in[j] = y + dy - y_dest + out.ticks_in[j] = y + dy out.in_amount += dy out.out_amount += x - out.admin_fee = unsafe_add(out.admin_fee, y_dest) - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: + if i != MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT - 1: if out.n2 == min_band: break if j == MAX_TICKS_UINT - 1: @@ -1035,7 +956,7 @@ def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: @internal @view -def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade: +def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> IAMM.DetailedTrade: """ @notice Method to use to calculate out amount and spent in amount @param i Input coin index @@ -1047,7 +968,7 @@ def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedT # i = 0: borrowable (USD) in, collateral (ETH) out; going up # i = 1: collateral (ETH) in, borrowable (USD) out; going down assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index" - out: DetailedTrade = empty(DetailedTrade) + out: IAMM.DetailedTrade = empty(IAMM.DetailedTrade) if amount == 0: return out in_precision: uint256 = COLLATERAL_PRECISION @@ -1067,7 +988,7 @@ def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedT @external @view -@nonreentrant('lock') +@nonreentrant def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: """ @notice Method to use to calculate out amount @@ -1081,7 +1002,7 @@ def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256): """ @notice Method to use to calculate out amount and spent in amount @@ -1090,7 +1011,7 @@ def get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256): @param in_amount Amount of input coin to swap @return A tuple with in_amount used and out_amount returned """ - out: DetailedTrade = self._get_dxdy(i, j, in_amount, True) + out: IAMM.DetailedTrade = self._get_dxdy(i, j, in_amount, True) return (out.in_amount, out.out_amount) @@ -1111,11 +1032,11 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ if amount == 0: return [0, 0] - lm: LMGauge = self.liquidity_mining_callback + lm: ILMGauge = self._liquidity_mining_callback collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = [] - in_coin: ERC20 = BORROWED_TOKEN - out_coin: ERC20 = COLLATERAL_TOKEN + in_coin: IERC20 = BORROWED_TOKEN + out_coin: IERC20 = COLLATERAL_TOKEN in_precision: uint256 = BORROWED_PRECISION out_precision: uint256 = COLLATERAL_PRECISION if i == 1: @@ -1124,7 +1045,7 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ out_precision = BORROWED_PRECISION out_coin = BORROWED_TOKEN - out: DetailedTrade = empty(DetailedTrade) + out: IAMM.DetailedTrade = empty(IAMM.DetailedTrade) if use_in_amount: out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision) else: @@ -1141,17 +1062,11 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ if out_amount_done == 0 or in_amount_done == 0: return [0, 0] - out.admin_fee = unsafe_div(out.admin_fee, in_precision) - if i == 0: - self.admin_fees_x += out.admin_fee - else: - self.admin_fees_y += out.admin_fee - n: int256 = min(out.n1, out.n2) n_start: int256 = n n_diff: int256 = abs(unsafe_sub(out.n2, out.n1)) - for k in range(MAX_TICKS): + for k: int256 in range(MAX_TICKS): x: uint256 = 0 y: uint256 = 0 if i == 0: @@ -1175,28 +1090,28 @@ def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _ self.active_band = out.n2 - log TokenExchange(_for, i, in_amount_done, j, out_amount_done) + log IAMM.TokenExchange(buyer=_for, sold_id=i, tokens_sold=in_amount_done, bought_id=j, tokens_bought=out_amount_done) if lm.address != empty(address): success: bool = False res: Bytes[32] = empty(Bytes[32]) success, res = raw_call( lm.address, - _abi_encode( + abi_encode( n_start, collateral_shares, len(collateral_shares), method_id=method_id("callback_collateral_shares(int256,uint256[],uint256)") ), max_outsize=32, revert_on_failure=False) - assert in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True) - assert out_coin.transfer(_for, out_amount_done, default_return_value=True) + tkn.transfer_from(in_coin, msg.sender, self, in_amount_done) + tkn.transfer(out_coin, _for, out_amount_done) return [in_amount_done, out_amount_done] @internal @view -def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade: +def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> IAMM.DetailedTrade: """ @notice Calculate the input amount required to receive the desired output amount. If couldn't exchange all - will also update the amount which was actually received. @@ -1212,7 +1127,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: # pump = False: collateral (ETH) in, borrowable (USD) out; going down min_band: int256 = self.min_band max_band: int256 = self.max_band - out: DetailedTrade = empty(DetailedTrade) + out: IAMM.DetailedTrade = empty(IAMM.DetailedTrade) out.n2 = self.active_band p_o_up: uint256 = self._p_oracle_up(out.n2) x: uint256 = self.bands_x[out.n2] @@ -1220,10 +1135,9 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out_amount_left: uint256 = out_amount fee: uint256 = max(self.fee, p_o[1]) - admin_fee: uint256 = self.admin_fee j: uint256 = MAX_TICKS_UINT - for i in range(MAX_TICKS + MAX_SKIP_TICKS): + for i: uint256 in range(MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT): y0: uint256 = 0 f: uint256 = 0 g: uint256 = 0 @@ -1235,7 +1149,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out.n1 = out.n2 j = 0 y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o - f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18) + f = unsafe_div(A * y0 * p_o[0] // p_o_up * p_o[0], 10**18) g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0]) Inv = (f + x) * (g + y) dynamic_fee = max(self.get_dynamic_fee(p_o[0], p_o_up), fee) @@ -1261,13 +1175,11 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: if y >= out_amount_left: # This is the last band out.last_tick_j = unsafe_sub(y, out_amount_left) - x_dest: uint256 = Inv / (g + out.last_tick_j) - f - x + x_dest: uint256 = Inv // (g + out.last_tick_j) - f - x dx: uint256 = unsafe_div(x_dest * antifee, 10**18) # MORE than x_dest out.out_amount = out_amount # We successfully found liquidity for all the out_amount out.in_amount += dx - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = x + dx - x_dest - out.admin_fee = unsafe_add(out.admin_fee, x_dest) + out.ticks_in[j] = x + dx break else: @@ -1277,11 +1189,9 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out_amount_left -= y out.in_amount += dx out.out_amount += y - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = x + dx - x_dest - out.admin_fee = unsafe_add(out.admin_fee, x_dest) + out.ticks_in[j] = x + dx - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: + if i != MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT - 1: if out.n2 == max_band: break if j == MAX_TICKS_UINT - 1: @@ -1300,13 +1210,11 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: if x >= out_amount_left: # This is the last band out.last_tick_j = unsafe_sub(x, out_amount_left) - y_dest: uint256 = Inv / (f + out.last_tick_j) - g - y + y_dest: uint256 = Inv // (f + out.last_tick_j) - g - y dy: uint256 = unsafe_div(y_dest * antifee, 10**18) # MORE than y_dest out.out_amount = out_amount out.in_amount += dy - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = y + dy - y_dest - out.admin_fee = unsafe_add(out.admin_fee, y_dest) + out.ticks_in[j] = y + dy break else: @@ -1316,11 +1224,9 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: out_amount_left -= x out.in_amount += dy out.out_amount += x - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = y + dy - y_dest - out.admin_fee = unsafe_add(out.admin_fee, y_dest) + out.ticks_in[j] = y + dy - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: + if i != MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT - 1: if out.n2 == min_band: break if j == MAX_TICKS_UINT - 1: @@ -1346,7 +1252,7 @@ def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: @external @view -@nonreentrant('lock') +@nonreentrant def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: """ @notice Method to use to calculate in amount required to receive the desired out_amount @@ -1357,14 +1263,14 @@ def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: """ # i = 0: borrowable (USD) in, collateral (ETH) out; going up # i = 1: collateral (ETH) in, borrowable (USD) out; going down - trade: DetailedTrade = self._get_dxdy(i, j, out_amount, False) + trade: IAMM.DetailedTrade = self._get_dxdy(i, j, out_amount, False) assert trade.out_amount == out_amount return trade.in_amount @external @view -@nonreentrant('lock') +@nonreentrant def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): """ @notice Method to use to calculate in amount required and out amount received @@ -1375,12 +1281,12 @@ def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): """ # i = 0: borrowable (USD) in, collateral (ETH) out; going up # i = 1: collateral (ETH) in, borrowable (USD) out; going down - out: DetailedTrade = self._get_dxdy(i, j, out_amount, False) + out: IAMM.DetailedTrade = self._get_dxdy(i, j, out_amount, False) return (out.out_amount, out.in_amount) @external -@nonreentrant('lock') +@nonreentrant def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address = msg.sender) -> uint256[2]: """ @notice Exchanges two coins, callable by anyone @@ -1395,7 +1301,7 @@ def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _f @external -@nonreentrant('lock') +@nonreentrant def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address = msg.sender) -> uint256[2]: """ @notice Exchanges two coins, callable by anyone @@ -1414,9 +1320,9 @@ def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256 def get_xy_up(user: address, use_y: bool) -> uint256: """ @notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up, - or the amount of x (stablecoin) if we trade adiabatically down + or the amount of x (borrowed) if we trade adiabatically down @param user User the amount is calculated for - @param use_y Calculate amount of collateral if True and of stablecoin if False + @param use_y Calculate amount of collateral if True and of borrowed if False @return Amount of coins """ ns: int256[2] = self._read_user_tick_numbers(user) @@ -1431,7 +1337,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: p_o_down: uint256 = self._p_oracle_up(ns[0]) XY: uint256 = 0 - for i in range(MAX_TICKS): + for i: uint256 in range(MAX_TICKS_UINT): n += 1 if n > ns[1]: break @@ -1461,7 +1367,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: # which is also more conservative # Also this will revert if p_o_down is 0, and p_o_down is 0 if p_o_up is 0 - p_current_mid: uint256 = unsafe_div(p_o**2 / p_o_down * p_o, p_o_up) + p_current_mid: uint256 = unsafe_div(p_o**2 // p_o_down * p_o, p_o_up) # if p_o > p_o_up - we "trade" everything to y and then convert to the result # if p_o < p_o_down - "trade" to x, then convert to result @@ -1475,7 +1381,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: # all to y at constant p_o, then to target currency adiabatically y_equiv: uint256 = y if y == 0: - y_equiv = x * 10**18 / p_current_mid + y_equiv = x * 10**18 // p_current_mid if use_y: XY += unsafe_div(y_equiv * user_share, total_share) else: @@ -1509,7 +1415,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: if p_o > p_o_up: # p_o < p_current_down, all to y # x_o = 0 - y_o = unsafe_sub(max(Inv / f, g), g) + y_o = unsafe_sub(max(Inv // f, g), g) if use_y: XY += unsafe_div(y_o * user_share, total_share) else: @@ -1517,7 +1423,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: elif p_o < p_o_down: # p_o > p_current_up, all to x # y_o = 0 - x_o = unsafe_sub(max(Inv / g, f), f) + x_o = unsafe_sub(max(Inv // g, f), f) if use_y: XY += unsafe_div(unsafe_div(x_o * SQRT_BAND_RATIO, p_o_up) * user_share, total_share) else: @@ -1529,11 +1435,11 @@ def get_xy_up(user: address, use_y: bool) -> uint256: # x_o = unsafe_div(A * y0 * p_o, p_o_up) * unsafe_sub(p_o_up, p_o) # Old math # y_o = unsafe_sub(max(self.sqrt_int(unsafe_div(Inv * 10**18, p_o)), g), g) - x_o = unsafe_sub(max(Inv / (g + y_o), f), f) + x_o = unsafe_sub(max(Inv // (g + y_o), f), f) # Now adiabatic conversion from definitely in-band if use_y: - XY += unsafe_div((y_o + x_o * 10**18 / self.sqrt_int(p_o_up * p_o)) * user_share, total_share) + XY += unsafe_div((y_o + x_o * 10**18 // self.sqrt_int(p_o_up * p_o)) * user_share, total_share) else: XY += unsafe_div((x_o + unsafe_div(y_o * self.sqrt_int(p_o_down * p_o), 10**18)) * user_share, total_share) @@ -1546,7 +1452,7 @@ def get_xy_up(user: address, use_y: bool) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def get_y_up(user: address) -> uint256: """ @notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up @@ -1558,10 +1464,10 @@ def get_y_up(user: address) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def get_x_down(user: address) -> uint256: """ - @notice Measure the amount of x (stablecoin) if we trade adiabatically down + @notice Measure the amount of x (borrowed) if we trade adiabatically down @param user User the amount is calculated for @return Amount of coins """ @@ -1571,10 +1477,10 @@ def get_x_down(user: address) -> uint256: @view def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2]: """ - @notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns + @notice A low-gas function to measure amounts of borrowed and collateral tokens which user currently owns @param user User address @param is_sum Return sum or amounts by bands - @return Amounts of (stablecoin, collateral) in a tuple + @return Amounts of (borrowed, collateral) in a tuple """ xs: DynArray[uint256, MAX_TICKS_UINT] = [] ys: DynArray[uint256, MAX_TICKS_UINT] = [] @@ -1584,7 +1490,7 @@ def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2] ns: int256[2] = self._read_user_tick_numbers(user) ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns) if ticks[0] != 0: - for i in range(MAX_TICKS): + for i: uint256 in range(MAX_TICKS_UINT): total_shares: uint256 = self.total_shares[ns[0]] + DEAD_SHARES ds: uint256 = ticks[i] dx: uint256 = unsafe_div((self.bands_x[ns[0]] + 1) * ds, total_shares) @@ -1607,31 +1513,31 @@ def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2] @external @view -@nonreentrant('lock') +@nonreentrant def get_sum_xy(user: address) -> uint256[2]: """ - @notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns + @notice A low-gas function to measure amounts of borrowed and collateral tokens which user currently owns @param user User address - @return Amounts of (stablecoin, collateral) in a tuple + @return Amounts of (borrowed, collateral) in a tuple """ xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True) return [xy[0][0], xy[1][0]] @external @view -@nonreentrant('lock') +@nonreentrant def get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]: """ - @notice A low-gas function to measure amounts of stablecoins and collateral by bands which user currently owns + @notice A low-gas function to measure amounts of borrowed and collateral tokens by bands which user currently owns @param user User address - @return Amounts of (stablecoin, collateral) by bands in a tuple + @return Amounts of (borrowed, collateral) by bands in a tuple """ return self._get_xy(user, False) @external @view -@nonreentrant('lock') +@nonreentrant def get_amount_for_price(p: uint256) -> (uint256, bool): """ @notice Amount necessary to be exchanged to have the AMM at the final price `p` @@ -1654,7 +1560,7 @@ def get_amount_for_price(p: uint256) -> (uint256, bool): fee: uint256 = max(self.fee, p_o[1]) - for i in range(MAX_TICKS + MAX_SKIP_TICKS): + for i: uint256 in range(MAX_TICKS_UINT + MAX_SKIP_TICKS_UINT): assert p_o_up > 0 x: uint256 = self.bands_x[n] y: uint256 = self.bands_y[n] @@ -1681,8 +1587,8 @@ def get_amount_for_price(p: uint256) -> (uint256, bool): if p <= p_up: if p >= p_down: if not_empty: - ynew: uint256 = unsafe_sub(max(self.sqrt_int(Inv * 10**18 / p), g), g) - xnew: uint256 = unsafe_sub(max(Inv / (g + ynew), f), f) + ynew: uint256 = unsafe_sub(max(self.sqrt_int(Inv * 10**18 // p), g), g) + xnew: uint256 = unsafe_sub(max(Inv // (g + ynew), f), f) if pump: amount += unsafe_div(unsafe_sub(max(xnew, x), x) * antifee, 10**18) else: @@ -1694,7 +1600,7 @@ def get_amount_for_price(p: uint256) -> (uint256, bool): if pump: if not_empty: - amount += unsafe_div(((Inv / g - f) - x) * antifee, 10**18) + amount += unsafe_div(((Inv // g - f) - x) * antifee, 10**18) if n == max_band: break if j == MAX_TICKS_UINT - 1: @@ -1709,7 +1615,7 @@ def get_amount_for_price(p: uint256) -> (uint256, bool): else: if not_empty: - amount += unsafe_div(((Inv / f - g) - y) * antifee, 10**18) + amount += unsafe_div(((Inv // f - g) - y) * antifee, 10**18) if n == min_band: break if j == MAX_TICKS_UINT - 1: @@ -1738,7 +1644,7 @@ def get_amount_for_price(p: uint256) -> (uint256, bool): @external -@nonreentrant('lock') +@nonreentrant def set_rate(rate: uint256) -> uint256: """ @notice Set interest rate. That affects the dependence of AMM base price over time @@ -1750,12 +1656,12 @@ def set_rate(rate: uint256) -> uint256: self.rate_mul = rate_mul self.rate_time = block.timestamp self.rate = rate - log SetRate(rate, rate_mul, block.timestamp) + log IAMM.SetRate(rate=rate, rate_mul=rate_mul, time=block.timestamp) return rate_mul @external -@nonreentrant('lock') +@nonreentrant def set_fee(fee: uint256): """ @notice Set AMM fee @@ -1763,38 +1669,27 @@ def set_fee(fee: uint256): """ assert msg.sender == self.admin self.fee = fee - log SetFee(fee) - - -@external -@nonreentrant('lock') -def set_admin_fee(fee: uint256): - """ - @notice Set admin fee - fraction of the AMM fee to go to admin - @param fee Admin fee where 1e18 == 100% - """ - assert msg.sender == self.admin - self.admin_fee = fee - log SetAdminFee(fee) + log IAMM.SetFee(fee=fee) +# nonreentrant decorator is in Controller which is admin @external -@nonreentrant('lock') -def reset_admin_fees(): +def set_callback(liquidity_mining_callback: ILMGauge): """ - @notice Zero out AMM fees collected + @notice Set a gauge address with callbacks for liquidity mining for collateral + @param liquidity_mining_callback Gauge address """ assert msg.sender == self.admin - self.admin_fees_x = 0 - self.admin_fees_y = 0 + self._liquidity_mining_callback = liquidity_mining_callback -# nonreentrant decorator is in Controller which is admin @external -def set_callback(liquidity_mining_callback: LMGauge): +@nonreentrant +def set_price_oracle(_price_oracle: IPriceOracle): """ - @notice Set a gauge address with callbacks for liquidity mining for collateral - @param liquidity_mining_callback Gauge address + @notice Set a new price oracle contract. Can only be called by admin (Controller) + @param _price_oracle New price oracle contract """ assert msg.sender == self.admin - self.liquidity_mining_callback = liquidity_mining_callback + self._price_oracle = _price_oracle + log IAMM.SetPriceOracle(price_oracle=_price_oracle) diff --git a/contracts/BoostedLMCallback.vy b/contracts/BoostedLMCallback.vy deleted file mode 100644 index ffe2b87f..00000000 --- a/contracts/BoostedLMCallback.vy +++ /dev/null @@ -1,456 +0,0 @@ -# @version 0.3.10 -""" -@title LlamaLend LMCallback -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice LM callback works like a gauge for collateral in LlamaLend/crvUSD AMMs -""" - -from vyper.interfaces import ERC20 - -interface LLAMMA: - def coins(i: uint256) -> address: view - def bands_x(n: int256) -> uint256: view - -interface VotingEscrowBoost: - def adjusted_balance_of(_account: address) -> uint256: view - -interface CRV20: - def future_epoch_time_write() -> uint256: nonpayable - def rate() -> uint256: view - -interface GaugeController: - def period() -> int128: view - def period_write() -> int128: nonpayable - def period_timestamp(p: int128) -> uint256: view - def gauge_relative_weight(addr: address, time: uint256) -> uint256: view - def voting_escrow() -> address: view - def checkpoint(): nonpayable - def checkpoint_gauge(addr: address): nonpayable - -interface Minter: - def token() -> address: view - def controller() -> address: view - def minted(user: address, gauge: address) -> uint256: view - - -event UpdateLiquidityLimit: - user: indexed(address) - original_balance: uint256 - original_supply: uint256 - working_balance: uint256 - working_supply: uint256 - - -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_TICKS_INT: constant(int256) = 50 -TOKENLESS_PRODUCTION: constant(uint256) = 40 -WEEK: constant(uint256) = 604800 - -AMM: public(immutable(LLAMMA)) -VECRV: public(immutable(ERC20)) -CRV: public(immutable(CRV20)) -VEBOOST_PROXY: public(immutable(VotingEscrowBoost)) -GAUGE_CONTROLLER: public(immutable(GaugeController)) -MINTER: public(immutable(Minter)) - -total_collateral: public(uint256) -working_supply: public(uint256) -user_boost: public(HashMap[address, uint256]) - -collateral_per_share: public(HashMap[int256, uint256]) -shares_per_band: public(HashMap[int256, uint256]) -working_shares_per_band: public(HashMap[int256, uint256]) # This only counts staked shares - -working_shares: public(HashMap[address, HashMap[int256, uint256]]) -user_shares: public(HashMap[address, HashMap[int256, uint256]]) -user_start_band: public(HashMap[address,int256]) -user_range_size: public(HashMap[address,int256]) - -# Tracking of mining period -inflation_rate: public(uint256) -future_epoch_time: public(uint256) - - -# Running integrals -# ------------------ -# Definitions: -# -# r - reward rate -# w - gauge relative weight -# s[i] - shares per band i -# cs[i] - collateral per share in band i -# s[u,i] - shares per user in band -# -# Reward rate per collateral: -# rrpc = (r * w) / sum(s[i] * cs[i]) -# -# Rewards per collateral (integral): -# I_rpc = integral(rrpc * dt) -# t_rpc - time of the last I_rpc value - -struct IntegralRPC: - rpc: uint256 - t: uint256 - -I_rpc: public(IntegralRPC) - -# Rewards per share: -# I_rps[i] = integral(cs[i] * rrpc * dt) = sum(cs[i] * delta(I_rpc)) - -struct IntegralRPS: - rps: uint256 - rpc: uint256 - -I_rps: public(HashMap[int256, IntegralRPS]) - -# Rewards per user: -# I_rpu[u,i] = sum(s[u,i] * delta(I_rps[i])) -# I_rpu[u] = sum_i(I_rpu[u,i]) - -struct IntegralRPU: - rpu: uint256 - rps: uint256 - -I_rpu: public(HashMap[address, HashMap[int256, IntegralRPU]]) -integrate_fraction: public(HashMap[address, uint256]) - - -@external -def __init__( - amm: LLAMMA, - crv: CRV20, - vecrv: ERC20, - veboost_proxy: VotingEscrowBoost, - gauge_controller: GaugeController, - minter: Minter, -): - """ - @notice BoostedLMCallback constructor. Should be deployed manually. - @param amm The address of amm - @param crv The address of CRV token - @param vecrv The address of veCRV - @param veboost_proxy The address of voting escrow proxy - @param gauge_controller The address of the gauge controller - @param minter the address of CRV minter - """ - AMM = amm - CRV = crv - VECRV = vecrv - VEBOOST_PROXY = veboost_proxy - GAUGE_CONTROLLER = gauge_controller - MINTER = minter - - self.inflation_rate = crv.rate() - self.future_epoch_time = crv.future_epoch_time_write() - - -@internal -def _update_liquidity_limit(user: address, collateral_amount: uint256, old_collateral_amount: uint256, is_underwater: bool) -> uint256: - """ - @notice Calculate limits which depend on the amount of CRV token per-user. - Effectively it calculates working collateral to apply amplification - of CRV production by CRV. - @dev Updates `total_collateral`, `working_supply` and `user_boost` - @param user The address of the user - @param collateral_amount User's amount of collateral BEFORE the checkpoint - @param old_collateral_amount User's amount of collateral AFTER the checkpoint - @param is_underwater Whether soft-liquidation for user's position started or not - @return User's boost: working_collateral / total_collateral [0.4, 1] - """ - # collateral_amount and total are used to calculate boosts - L: uint256 = max(self.total_collateral + collateral_amount, old_collateral_amount) - old_collateral_amount - self.total_collateral = L - - boost: uint256 = self.user_boost[user] - lim: uint256 = collateral_amount * boost / 10 ** 18 - if not is_underwater: - # To be called after totalSupply is updated - voting_balance: uint256 = VEBOOST_PROXY.adjusted_balance_of(user) - voting_total: uint256 = VECRV.totalSupply() - - lim = collateral_amount * TOKENLESS_PRODUCTION / 100 - if voting_total > 0: - lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 - lim = min(collateral_amount, lim) - - _working_supply: uint256 = self.working_supply + lim - old_collateral_amount * self.user_boost[user] / 10**18 - self.working_supply = _working_supply - if collateral_amount > 0 and not is_underwater: # Do not update boost for underwater user to prevent manipulation - boost = lim * 10**18 / collateral_amount - self.user_boost[user] = boost - - log UpdateLiquidityLimit(user, collateral_amount, L, lim, _working_supply) - - return boost - - -@internal -def _checkpoint_collateral_shares(n_start: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT], size: int256): - """ - @notice Checkpoint for shares in a set of bands - @dev Updates the CRV emission shares are entitled to receive - @param n_start Index of the first band to checkpoint - @param collateral_per_share Collateral per share ratio by bands - @param size The number of bands to checkpoint starting from `n_start` - """ - # Read current and new rate; update the new rate if needed - I_rpc: IntegralRPC = self.I_rpc - rate: uint256 = self.inflation_rate - new_rate: uint256 = rate - prev_future_epoch: uint256 = self.future_epoch_time - if block.timestamp >= prev_future_epoch: - self.future_epoch_time = CRV.future_epoch_time_write() - new_rate = CRV.rate() - self.inflation_rate = new_rate - - # * Record the collateral per share values - # * Record integrals of rewards per share - working_supply: uint256 = self.working_supply - total_collateral: uint256 = self.total_collateral - delta_rpc: uint256 = 0 - - if working_supply > 0 and block.timestamp > I_rpc.t: # XXX should we not loop when boosted collateral == 0? - GAUGE_CONTROLLER.checkpoint_gauge(self) - prev_week_time: uint256 = I_rpc.t - week_time: uint256 = min((I_rpc.t + WEEK) / WEEK * WEEK, block.timestamp) - - for week_iter in range(500): - dt: uint256 = week_time - prev_week_time - w: uint256 = GAUGE_CONTROLLER.gauge_relative_weight(self, prev_week_time / WEEK * WEEK) - - if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: - # If we went across one or multiple epochs, apply the rate - # of the first epoch until it ends, and then the rate of - # the last epoch. - # If more than one epoch is crossed - the gauge gets less, - # but that'd mean it wasn't called for more than 1 year - delta_rpc += rate * w * (prev_future_epoch - prev_week_time) / working_supply - rate = new_rate - delta_rpc += rate * w * (week_time - prev_future_epoch) / working_supply - else: - delta_rpc += rate * w * dt / working_supply - # On precisions of the calculation - # rate ~= 10e18 - # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) - # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) - # The largest loss is at dt = 1 - # Loss is 1e-9 - acceptable - - if week_time == block.timestamp: - break - prev_week_time = week_time - week_time = min(week_time + WEEK, block.timestamp) - - I_rpc.t = block.timestamp - I_rpc.rpc += delta_rpc - self.I_rpc = I_rpc - - # Update working_supply - for i in range(MAX_TICKS_INT): - if i == size: - break - _n: int256 = n_start + i - old_cps: uint256 = self.collateral_per_share[_n] - cps: uint256 = old_cps - if len(collateral_per_share) > 0: - cps = collateral_per_share[i] - self.collateral_per_share[_n] = cps - I_rps: IntegralRPS = self.I_rps[_n] - I_rps.rps += old_cps * (I_rpc.rpc - I_rps.rpc) / 10**18 - I_rps.rpc = I_rpc.rpc - self.I_rps[_n] = I_rps - if cps != old_cps: - wspb: uint256 = self.working_shares_per_band[_n] - spb: uint256 = self.shares_per_band[_n] - if wspb > 0: - # working_supply += wspb * (cps - old_cps) / 10**18 - old_working_supply: uint256 = wspb * old_cps / 10**18 - working_supply = max(working_supply + wspb * cps / 10**18, old_working_supply) - old_working_supply - old_total_collateral: uint256 = spb * old_cps / 10 ** 18 - total_collateral = max(total_collateral + spb * cps / 10**18, old_total_collateral) - old_total_collateral - - self.working_supply = working_supply - self.total_collateral = total_collateral - - -@internal -@view -def _user_amounts(user: address, n_start: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT], size: int256) -> uint256[3]: - """ - @notice Calculates user collateral amount from user bands and shares - @param user The address of the user - @param n_start Index of the first band to checkpoint - @param user_shares User's shares by bands - @param size The number of bands to checkpoint starting from `n_start` - @return [ - collateral_amount calculated from passed args, - old_collateral_amount, - stablecoin_amount (needed to check whether user is underwater or not), - ] - """ - old_collateral_amount: uint256 = 0 - collateral_amount: uint256 = 0 - stablecoin_amount: uint256 = 0 # this amount is needed just to check whether user is_underwater or not - if len(user_shares) > 0: - for i in range(MAX_TICKS_INT): - if i == size: - break - cps: uint256 = self.collateral_per_share[n_start + i] - old_collateral_amount += self.user_shares[user][n_start + i] * cps / 10**18 - collateral_amount += user_shares[i] * cps / 10**18 - stablecoin_amount += AMM.bands_x(n_start + i) - - return [collateral_amount, old_collateral_amount, stablecoin_amount] - else: - for i in range(MAX_TICKS_INT): - if i == size: - break - old_collateral_amount += self.user_shares[user][n_start + i] * self.collateral_per_share[n_start + i] / 10**18 - stablecoin_amount += AMM.bands_x(n_start + i) - - return [old_collateral_amount, old_collateral_amount, stablecoin_amount] - - -@internal -def _checkpoint_user_shares(user: address, n_start: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT], size: int256): - """ - @notice Checkpoint for user's shares in a set of bands - @dev Updates the CRV emissions a user is entitled to receive - @param user The address of the user - @param n_start Index of the first band to checkpoint - @param user_shares User's shares by bands - @param size The number of bands to checkpoint starting from `n_start` - """ - # Calculate the amount of real collateral for the user - _amounts: uint256[3] = self._user_amounts(user, n_start, user_shares, size) # [collateral_amount, old_collateral_amount, stablecoin_amount] - boost: uint256 = self._update_liquidity_limit(user, _amounts[0], _amounts[1], _amounts[2] > 0) - - rpu: uint256 = self.integrate_fraction[user] - - for i in range(MAX_TICKS_INT): - if i == size: - break - _n: int256 = n_start + i - old_ws: uint256 = self.working_shares[user][_n] - - if len(user_shares) > 0: - # Transition from working_balance to working_shares: - # 1. working_balance * real_shares == real_balance * working_shares - # 2. collateral_per_share * working_shares = working_balance - # - # It's needed to update working supply during soft-liquidation - self.shares_per_band[_n] = self.shares_per_band[_n] + user_shares[i] - self.user_shares[user][_n] - self.user_shares[user][_n] = user_shares[i] - - ws: uint256 = user_shares[i] * boost / 10**18 - self.working_shares[user][_n] = ws - self.working_shares_per_band[_n] = self.working_shares_per_band[_n] + ws - old_ws - else: # Here we just update boost - ws: uint256 = self.user_shares[user][_n] * boost / 10**18 - self.working_shares[user][_n] = ws - self.working_shares_per_band[_n] = self.working_shares_per_band[_n] + ws - old_ws - - - I_rpu: IntegralRPU = self.I_rpu[user][_n] - I_rps: uint256 = self.I_rps[_n].rps - d_rpu: uint256 = old_ws * (I_rps - I_rpu.rps) / 10**18 - I_rpu.rpu += d_rpu - rpu += d_rpu - I_rpu.rps = I_rps - self.I_rpu[user][_n] = I_rpu - - self.integrate_fraction[user] = rpu - - -@external -@view -def user_collateral(user: address) -> uint256: - """ - @param user The address of the user - @return User's collateral amount in LlamaLend/crvUSD AMM - """ - return self._user_amounts(user, self.user_start_band[user], [], self.user_range_size[user])[0] - - -@external -@view -def working_collateral(user: address) -> uint256: - """ - @param user The address of the user - @return User's working collateral amount [0.4 * user_collateral, user_collateral] - """ - n_start: int256 = self.user_start_band[user] - size: int256 = self.user_range_size[user] - _working_balance: uint256 = 0 - for i in range(MAX_TICKS_INT): - if i == size: - break - _working_balance += self.working_shares[user][n_start + i] * self.collateral_per_share[n_start + i] / 10 ** 18 - - return _working_balance - - -@external -def callback_collateral_shares(n_start: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT], size: uint256): - """ - @notice Checkpoint for shares in a set of bands - @dev Updates the CRV emission shares are entitled to receive. - Can be called only be the corresponding AMM. - It is important that this callback is called every time before callback_user_shares. - @param n_start Index of the first band to checkpoint - @param collateral_per_share Collateral per share ratio by bands - @param size The number of bands to checkpoint starting from `n_start` - """ - # It is important that this callback is called every time before callback_user_shares - assert msg.sender == AMM.address - self._checkpoint_collateral_shares(n_start, collateral_per_share, convert(size, int256)) - - -@external -def callback_user_shares(user: address, n_start: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT]): - """ - @notice Checkpoint for user's shares in a set of bands. - @dev Updates the CRV emissions a user is entitled to receive. - Can be called only be the corresponding AMM. - @param user The address of the user - @param n_start Index of the first band to checkpoint - @param user_shares User's shares by bands - """ - assert msg.sender == AMM.address - self.user_start_band[user] = n_start - size: int256 = convert(len(user_shares), int256) - self.user_range_size[user] = size - self._checkpoint_user_shares(user, n_start, user_shares, size) - - -@external -def user_checkpoint(addr: address) -> bool: - """ - @notice Record a checkpoint for `addr` - @dev Can be called only by user or CRV minter - @param addr User address - @return bool success - """ - n_start: int256 = self.user_start_band[addr] - size: int256 = self.user_range_size[addr] - self._checkpoint_collateral_shares(n_start, [], size) - self._checkpoint_user_shares(addr, n_start, [], size) - - return True - - -@external -def claimable_tokens(addr: address) -> uint256: - """ - @notice Get the number of claimable tokens per user - @dev This function should be manually changed to "view" in the ABI - @param addr User address - @return uint256 number of claimable tokens per user - """ - n_start: int256 = self.user_start_band[addr] - size: int256 = self.user_range_size[addr] - self._checkpoint_collateral_shares(n_start, [], size) - self._checkpoint_user_shares(addr, n_start, [], size) - - return self.integrate_fraction[addr] - MINTER.minted(addr, self) diff --git a/contracts/Controller.vy b/contracts/Controller.vy index 02aa1960..47add5cf 100644 --- a/contracts/Controller.vy +++ b/contracts/Controller.vy @@ -1,357 +1,323 @@ -# @version 0.3.10 +# pragma version 0.4.3 +# pragma nonreentrancy on # pragma optimize codesize -# pragma evm-version shanghai """ -@title crvUSD Controller +@title Llamalend Mint Market Controller @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice Main contract to interact with a Llamalend Mint Market. Each + contract is specific to a single mint market. +@custom:security security@curve.fi """ -interface LLAMMA: - def A() -> uint256: view - def get_p() -> uint256: view - def get_base_price() -> uint256: view - def active_band() -> int256: view - def active_band_with_skip() -> int256: view - def p_oracle_up(n: int256) -> uint256: view - def p_oracle_down(n: int256) -> uint256: view - def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): nonpayable - def read_user_tick_numbers(_for: address) -> int256[2]: view - def get_sum_xy(user: address) -> uint256[2]: view - def withdraw(user: address, frac: uint256) -> uint256[2]: nonpayable - def get_x_down(user: address) -> uint256: view - def get_rate_mul() -> uint256: view - def set_rate(rate: uint256) -> uint256: nonpayable - def set_fee(fee: uint256): nonpayable - def set_admin_fee(fee: uint256): nonpayable - def price_oracle() -> uint256: view - def can_skip_bands(n_end: int256) -> bool: view - def admin_fees_x() -> uint256: view - def admin_fees_y() -> uint256: view - def reset_admin_fees(): nonpayable - def has_liquidity(user: address) -> bool: view - def bands_x(n: int256) -> uint256: view - def bands_y(n: int256) -> uint256: view - def set_callback(user: address): nonpayable - -interface ERC20: - def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable - def transfer(_to: address, _value: uint256) -> bool: nonpayable - def decimals() -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - def balanceOf(_from: address) -> uint256: view - -interface MonetaryPolicy: - def rate_write() -> uint256: nonpayable - -interface Factory: - def stablecoin() -> address: view - def admin() -> address: view - def fee_receiver() -> address: view - - # Only if lending vault - def borrowed_token() -> address: view - def collateral_token() -> address: view - - -event UserState: - user: indexed(address) - collateral: uint256 - debt: uint256 - n1: int256 - n2: int256 - liquidation_discount: uint256 - -event Borrow: - user: indexed(address) - collateral_increase: uint256 - loan_increase: uint256 - -event Repay: - user: indexed(address) - collateral_decrease: uint256 - loan_decrease: uint256 - -event RemoveCollateral: - user: indexed(address) - collateral_decrease: uint256 - -event Liquidate: - liquidator: indexed(address) - user: indexed(address) - collateral_received: uint256 - stablecoin_received: uint256 - debt: uint256 - -event SetMonetaryPolicy: - monetary_policy: address - -event SetBorrowingDiscounts: - loan_discount: uint256 - liquidation_discount: uint256 - -event SetExtraHealth: - user: indexed(address) - health: uint256 - -event CollectFees: - amount: uint256 - new_supply: uint256 - -event SetLMCallback: - callback: address - -event Approval: - owner: indexed(address) - spender: indexed(address) - allow: bool - - -struct Loan: - initial_debt: uint256 - rate_mul: uint256 - -struct Position: - user: address - x: uint256 - y: uint256 - debt: uint256 - health: int256 - -struct CallbackData: - active_band: int256 - stablecoins: uint256 - collateral: uint256 - - -FACTORY: immutable(Factory) +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import ILMGauge +from contracts.interfaces import IFactory +from contracts.interfaces import IPriceOracle +from curve_std.interfaces import IERC20 + +from contracts.interfaces import IController +from contracts.interfaces import IControllerView as IView + +implements: IController +implements: IView + +from curve_std import token as tkn +from curve_std import math as crv_math + +from snekmate.utils import math + +################################################################ +# IMMUTABLES # +################################################################ + +AMM: immutable(IAMM) +MAX_AMM_FEE: immutable(uint256) +A: immutable(uint256) +# log(A / (A - 1)) +LOGN_A_RATIO: immutable(int256) +SQRT_BAND_RATIO: immutable(uint256) + +COLLATERAL_TOKEN: immutable(IERC20) +COLLATERAL_PRECISION: immutable(uint256) +BORROWED_TOKEN: immutable(IERC20) +BORROWED_PRECISION: immutable(uint256) +FACTORY: immutable(IFactory) + +################################################################ +# CONSTANTS # +################################################################ + +version: public(constant(String[5])) = c.__version__ + +from contracts import constants as c + +# https://github.com/vyperlang/vyper/issues/4723 +WAD: constant(uint256) = c.WAD +SWAD: constant(int256) = c.SWAD +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES +MIN_TICKS_UINT: constant(uint256) = c.MIN_TICKS_UINT + +MIN_AMM_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 + +CALLBACK_DEPOSIT: constant(bytes4) = method_id( + "callback_deposit(address,uint256,uint256,uint256,bytes)", + output_type=bytes4, +) +CALLBACK_REPAY: constant(bytes4) = method_id( + "callback_repay(address,uint256,uint256,uint256,bytes)", output_type=bytes4 +) +CALLBACK_LIQUIDATE: constant(bytes4) = method_id( + "callback_liquidate(address,uint256,uint256,uint256,bytes)", + output_type=bytes4, +) +CALLDATA_MAX_SIZE: constant(uint256) = c.CALLDATA_MAX_SIZE + MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 # Start liquidating when threshold reached +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT MIN_TICKS: constant(int256) = 4 -MIN_TICKS_UINT: constant(uint256) = 4 MAX_SKIP_TICKS: constant(uint256) = 1024 MAX_P_BASE_BANDS: constant(int256) = 5 MAX_RATE: constant(uint256) = 43959106799 # 300% APY +MAX_ORACLE_PRICE_DEVIATION: constant(uint256) = WAD // 2 # 50% deviation -loan: HashMap[address, Loan] -liquidation_discounts: public(HashMap[address, uint256]) -_total_debt: Loan - -loans: public(address[2**64 - 1]) # Enumerate existing loans -loan_ix: public(HashMap[address, uint256]) # Position of the loan in the list -n_loans: public(uint256) # Number of nonzero loans +################################################################ +# STORAGE # +################################################################ -minted: public(uint256) -redeemed: public(uint256) +view_impl: public(address) +_view: IView -monetary_policy: public(MonetaryPolicy) liquidation_discount: public(uint256) loan_discount: public(uint256) +_monetary_policy: IMonetaryPolicy -COLLATERAL_TOKEN: immutable(ERC20) -COLLATERAL_PRECISION: immutable(uint256) +# https://github.com/vyperlang/vyper/issues/4721 +@external +@view +def monetary_policy() -> IMonetaryPolicy: + """ + @notice Address of the monetary policy + """ + return self._monetary_policy -BORROWED_TOKEN: immutable(ERC20) -BORROWED_PRECISION: immutable(uint256) -AMM: immutable(LLAMMA) -A: immutable(uint256) -Aminus1: immutable(uint256) -LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) +approval: public(HashMap[address, HashMap[address, bool]]) +extra_health: public(HashMap[address, uint256]) -MAX_ADMIN_FEE: constant(uint256) = 5 * 10**17 # 50% -MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MAX_FEE: immutable(uint256) # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 +loan: HashMap[address, IController.Loan] +liquidation_discounts: public(HashMap[address, uint256]) +_total_debt: IController.Loan -CALLBACK_DEPOSIT: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[])", output_type=bytes4) -CALLBACK_REPAY: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[])", output_type=bytes4) -CALLBACK_LIQUIDATE: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[])", output_type=bytes4) +# Enumerate existing loans +loans: public(address[2**64 - 1]) +# Position of the loan in the list +loan_ix: public(HashMap[address, uint256]) +# Number of nonzero loans +n_loans: public(uint256) -CALLBACK_DEPOSIT_WITH_BYTES: constant(bytes4) = method_id("callback_deposit(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) -# CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = method_id("callback_repay(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) <-- BUG! The reason is 0 at the beginning of method_id -CALLBACK_REPAY_WITH_BYTES: constant(bytes4) = 0x008ae188 -CALLBACK_LIQUIDATE_WITH_BYTES: constant(bytes4) = method_id("callback_liquidate(address,uint256,uint256,uint256,uint256[],bytes)", output_type=bytes4) -DEAD_SHARES: constant(uint256) = 1000 +# cumulative amount of borrowed assets ever lent +lent: uint256 +# cumulative amount of borrowed assets ever repaid +repaid: uint256 +# cumulative amount of borrowed assets ever collected as admin fees +collected: uint256 -approval: public(HashMap[address, HashMap[address, bool]]) -extra_health: public(HashMap[address, uint256]) +# Admin fees yet to be collected. Goes to zero when collected. +admin_fees: public(uint256) +admin_percentage: public(uint256) -@external +# DANGER DO NOT RELY ON MSG.SENDER IN VIRTUAL METHODS +interface VirtualMethods: + def _on_debt_increased(delta: uint256, total_debt: uint256): nonpayable + +implements: VirtualMethods + +VIRTUAL: immutable(VirtualMethods) + + +@deploy def __init__( - collateral_token: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256, - amm: address): - """ - @notice Controller constructor deployed by the factory from blueprint - @param collateral_token Token to use for collateral - @param monetary_policy Address of monetary policy - @param loan_discount Discount of the maximum loan size compare to get_x_down() value - @param liquidation_discount Discount of the maximum loan size compare to - get_x_down() for "bad liquidation" purposes - @param amm AMM address (Already deployed from blueprint) - """ - FACTORY = Factory(msg.sender) + _collateral_token: IERC20, + _borrowed_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + _AMM: IAMM, + view_impl: address, +): + VIRTUAL = VirtualMethods(self) - self.monetary_policy = MonetaryPolicy(monetary_policy) + # TODO add sanity check for zero addresses + # In MintController the correct way to limit borrowing + # is through the debt ceiling. This is here to be used + # in LendController only. - self.liquidation_discount = liquidation_discount - self.loan_discount = loan_discount - self._total_debt.rate_mul = 10**18 - - AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() - A = _A - Aminus1 = unsafe_sub(_A, 1) - LOGN_A_RATIO = self.wad_ln(unsafe_div(_A * 10**18, unsafe_sub(_A, 1))) - MAX_FEE = min(unsafe_div(10**18 * MIN_TICKS, A), 10**17) - - _collateral_token: ERC20 = ERC20(collateral_token) - _borrowed_token: ERC20 = empty(ERC20) - - if collateral_token == empty(address): - # Lending vault factory - _collateral_token = ERC20(Factory(msg.sender).collateral_token()) - _borrowed_token = ERC20(Factory(msg.sender).borrowed_token()) - else: - # Stablecoin factory - # _collateral_token is already set - _borrowed_token = ERC20(Factory(msg.sender).stablecoin()) + FACTORY = IFactory(msg.sender) + AMM = _AMM + + A = staticcall AMM.A() + + LOGN_A_RATIO = math._wad_ln(convert(A * WAD // (A - 1), int256)) + SQRT_BAND_RATIO = isqrt(10**36 * A // (A - 1)) + + # let's set to MIN_TICKS / A: for example, 4% max fee for A=100 + MAX_AMM_FEE = min(WAD * MIN_TICKS_UINT // A, 10**17) COLLATERAL_TOKEN = _collateral_token + collateral_decimals: uint256 = convert(staticcall COLLATERAL_TOKEN.decimals(), uint256) + COLLATERAL_PRECISION = pow_mod256(10, 18 - collateral_decimals) + BORROWED_TOKEN = _borrowed_token - COLLATERAL_PRECISION = pow_mod256(10, 18 - _collateral_token.decimals()) - BORROWED_PRECISION = pow_mod256(10, 18 - _borrowed_token.decimals()) + borrowed_decimals: uint256 = convert(staticcall BORROWED_TOKEN.decimals(), uint256) + BORROWED_PRECISION = pow_mod256(10, 18 - borrowed_decimals) - SQRT_BAND_RATIO = isqrt(unsafe_div(10**36 * _A, unsafe_sub(_A, 1))) + # This is useless for lending markets, but leaving it doesn't create any harm + tkn.max_approve(BORROWED_TOKEN, FACTORY.address) - assert _borrowed_token.approve(msg.sender, max_value(uint256), default_return_value=True) + self._monetary_policy = monetary_policy + self.liquidation_discount = liquidation_discount + self.loan_discount = loan_discount + self._total_debt.rate_mul = WAD + self.admin_percentage = WAD + self._set_view(view_impl) + + +@external +def set_view(view_impl: address): + """ + @notice Change the contract used to store view functions. + @dev This function deploys a new view implementation from a blueprint. + @param view_impl Address of the new view implementation + """ + self._check_admin() + self._set_view(view_impl) @internal -@pure -def _log_2(x: uint256) -> uint256: - """ - @dev An `internal` helper function that returns the log in base 2 - of `x`, following the selected rounding direction. - @notice Note that it returns 0 if given 0. The implementation is - inspired by OpenZeppelin's implementation here: - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol. - This code is taken from snekmate. - @param x The 32-byte variable. - @return uint256 The 32-byte calculation result. - """ - value: uint256 = x - result: uint256 = empty(uint256) - - # The following lines cannot overflow because we have the well-known - # decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`. - if (x >> 128 != empty(uint256)): - value = x >> 128 - result = 128 - if (value >> 64 != empty(uint256)): - value = value >> 64 - result = unsafe_add(result, 64) - if (value >> 32 != empty(uint256)): - value = value >> 32 - result = unsafe_add(result, 32) - if (value >> 16 != empty(uint256)): - value = value >> 16 - result = unsafe_add(result, 16) - if (value >> 8 != empty(uint256)): - value = value >> 8 - result = unsafe_add(result, 8) - if (value >> 4 != empty(uint256)): - value = value >> 4 - result = unsafe_add(result, 4) - if (value >> 2 != empty(uint256)): - value = value >> 2 - result = unsafe_add(result, 2) - if (value >> 1 != empty(uint256)): - result = unsafe_add(result, 1) - - return result +def _set_view(view_impl: address): + """ + @notice Set the view implementation + @param view New view implementation + """ + assert view_impl != empty(address) # dev: view implementation is empty address + self.view_impl = view_impl + view: address = create_from_blueprint( + view_impl, + self, + SQRT_BAND_RATIO, + LOGN_A_RATIO, + AMM, + A, + COLLATERAL_TOKEN, + COLLATERAL_PRECISION, + BORROWED_TOKEN, + BORROWED_PRECISION, + ) + self._view = IView(view) + + log IController.SetView(view=view) + + +@view +@external +def minted() -> uint256: + return self.lent + self.collected + + +@view +@external +def redeemed() -> uint256: + return self.repaid @internal -@pure -def wad_ln(x: uint256) -> int256: - """ - @dev Calculates the natural logarithm of a signed integer with a - precision of 1e18. - @notice Note that it returns 0 if given 0. Furthermore, this function - consumes about 1,400 to 1,650 gas units depending on the value - of `x`. The implementation is inspired by Remco Bloemen's - implementation under the MIT license here: - https://xn--2-umb.com/22/exp-ln. - This code is taken from snekmate. - @param x The 32-byte variable. - @return int256 The 32-byte calculation result. - """ - value: int256 = convert(x, int256) - - assert x > 0 - - # We want to convert `x` from "10 ** 18" fixed point to "2 ** 96" - # fixed point. We do this by multiplying by "2 ** 96 / 10 ** 18". - # But since "ln(x * C) = ln(x) + ln(C)" holds, we can just do nothing - # here and add "ln(2 ** 96 / 10 ** 18)" at the end. - - # Reduce the range of `x` to "(1, 2) * 2 ** 96". - # Also remember that "ln(2 ** k * x) = k * ln(2) + ln(x)" holds. - k: int256 = unsafe_sub(convert(self._log_2(x), int256), 96) - # Note that to circumvent Vyper's safecast feature for the potentially - # negative expression `value <<= uint256(159 - k)`, we first convert the - # expression `value <<= uint256(159 - k)` to `bytes32` and subsequently - # to `uint256`. Remember that the EVM default behaviour is to use two's - # complement representation to handle signed integers. - value = convert(convert(convert(value << convert(unsafe_sub(159, k), uint256), bytes32), uint256) >> 159, int256) - - # Evaluate using a "(8, 8)"-term rational approximation. Since `p` is monic, - # we will multiply by a scaling factor later. - p: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 3_273_285_459_638_523_848_632_254_066_296), value) >> 96, 24_828_157_081_833_163_892_658_089_445_524) - p = unsafe_add(unsafe_mul(p, value) >> 96, 43_456_485_725_739_037_958_740_375_743_393) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 11_111_509_109_440_967_052_023_855_526_967) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 45_023_709_667_254_063_763_336_534_515_857) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 14_706_773_417_378_608_786_704_636_184_526) - p = unsafe_sub(unsafe_mul(p, value), 795_164_235_651_350_426_258_249_787_498 << 96) - - # We leave `p` in the "2 ** 192" base so that we do not have to scale it up - # again for the division. Note that `q` is monic by convention. - q: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 5_573_035_233_440_673_466_300_451_813_936), value) >> 96, 71_694_874_799_317_883_764_090_561_454_958) - q = unsafe_add(unsafe_mul(q, value) >> 96, 283_447_036_172_924_575_727_196_451_306_956) - q = unsafe_add(unsafe_mul(q, value) >> 96, 401_686_690_394_027_663_651_624_208_769_553) - q = unsafe_add(unsafe_mul(q, value) >> 96, 204_048_457_590_392_012_362_485_061_816_622) - q = unsafe_add(unsafe_mul(q, value) >> 96, 31_853_899_698_501_571_402_653_359_427_138) - q = unsafe_add(unsafe_mul(q, value) >> 96, 909_429_971_244_387_300_277_376_558_375) - - # It is known that the polynomial `q` has no zeros in the domain. - # No scaling is required, as `p` is already "2 ** 96" too large. Also, - # `r` is in the range "(0, 0.125) * 2 ** 96" after the division. - r: int256 = unsafe_div(p, q) - - # To finalise the calculation, we have to proceed with the following steps: - # - multiply by the scaling factor "s = 5.549...", - # - add "ln(2 ** 96 / 10 ** 18)", - # - add "k * ln(2)", and - # - multiply by "10 ** 18 / 2 ** 96 = 5 ** 18 >> 78". - # In order to perform the most gas-efficient calculation, we carry out all - # these steps in one expression. - return unsafe_add(unsafe_add(unsafe_mul(r, 1_677_202_110_996_718_588_342_820_967_067_443_963_516_166),\ - unsafe_mul(k, 16_597_577_552_685_614_221_487_285_958_193_947_469_193_820_559_219_878_177_908_093_499_208_371)),\ - 600_920_179_829_731_861_736_702_779_321_621_459_595_472_258_049_074_101_567_377_883_020_018_308) >> 174 +@view +def _check_admin(): + assert msg.sender == staticcall FACTORY.admin(), "only admin" + + +@internal +@view +def _get_total_debt() -> uint256: + """ + @notice Total debt of this controller + """ + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self._total_debt + return loan.initial_debt * rate_mul // loan.rate_mul + + +@internal +def _update_total_debt( + d_debt: uint256, rate_mul: uint256, is_increase: bool +) -> IController.Loan: + """ + @notice Update total debt of this controller + @dev This method MUST be called strictly BEFORE lent, repaid or collected change + @param d_debt Change in debt amount (unsigned) + @param rate_mul New rate_mul + @param is_increase Whether debt increases or decreases + """ + loan: IController.Loan = self._total_debt + loan_with_interest: uint256 = loan.initial_debt * rate_mul // loan.rate_mul + accrued_interest: uint256 = loan_with_interest - loan.initial_debt + accrued_admin_fees: uint256 = accrued_interest * self.admin_percentage // WAD + self.admin_fees += accrued_admin_fees + loan.initial_debt = loan_with_interest + if is_increase: + loan.initial_debt += d_debt + extcall VIRTUAL._on_debt_increased(d_debt, loan.initial_debt) + else: + loan.initial_debt = crv_math.sub_or_zero(loan.initial_debt, d_debt) + loan.rate_mul = rate_mul + self._total_debt = loan + + return loan @external -@pure -def factory() -> Factory: +def set_price_oracle(price_oracle: IPriceOracle, max_deviation: uint256): + """ + @notice Set a new price oracle for the AMM + @param price_oracle New price oracle contract + @param max_deviation Maximum allowed deviation for the new oracle + Can be set to max_value(uint256) to skip the check if oracle is broken. + """ + self._check_admin() + assert ( + max_deviation <= MAX_ORACLE_PRICE_DEVIATION + or max_deviation == max_value(uint256) + ) # dev: invalid max deviation + + # Validate the new oracle has required methods + extcall price_oracle.price_w() + new_price: uint256 = staticcall price_oracle.price() + + # Check price deviation isn't too high + current_oracle: IPriceOracle = staticcall AMM.price_oracle_contract() + old_price: uint256 = staticcall current_oracle.price() + if max_deviation != max_value(uint256): + delta: uint256 = ( + new_price - old_price + if old_price < new_price + else old_price - new_price + ) + max_delta: uint256 = old_price * max_deviation // WAD + assert delta <= max_delta, "delta>max" + + extcall AMM.set_price_oracle(price_oracle) + + +@external +@view +def factory() -> IFactory: """ @notice Address of the factory """ @@ -359,8 +325,9 @@ def factory() -> Factory: @external -@pure -def amm() -> LLAMMA: +@view +@reentrant +def amm() -> IAMM: """ @notice Address of the AMM """ @@ -368,8 +335,9 @@ def amm() -> LLAMMA: @external -@pure -def collateral_token() -> ERC20: +@view +@reentrant +def collateral_token() -> IERC20: """ @notice Address of the collateral token """ @@ -377,8 +345,9 @@ def collateral_token() -> ERC20: @external -@pure -def borrowed_token() -> ERC20: +@view +@reentrant +def borrowed_token() -> IERC20: """ @notice Address of the borrowed token """ @@ -390,12 +359,11 @@ def _save_rate(): """ @notice Save current rate """ - rate: uint256 = min(self.monetary_policy.rate_write(), MAX_RATE) - AMM.set_rate(rate) + rate: uint256 = min(extcall self._monetary_policy.rate_write(), MAX_RATE) + extcall AMM.set_rate(rate) @external -@nonreentrant('lock') def save_rate(): """ @notice Save current rate @@ -411,8 +379,8 @@ def _debt(user: address) -> (uint256, uint256): @param user User address @return (debt, rate_mul) """ - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self.loan[user] + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self.loan[user] if loan.initial_debt == 0: return (0, rate_mul) else: @@ -422,13 +390,14 @@ def _debt(user: address) -> (uint256, uint256): if debt % loan.rate_mul > 0: # if only one loan -> don't have to do it if self.n_loans > 1: debt += unsafe_sub(loan.rate_mul, 1) - debt = unsafe_div(debt, loan.rate_mul) # loan.rate_mul is nonzero because we just had % successful + debt = unsafe_div( + debt, loan.rate_mul + ) # loan.rate_mul is nonzero because we just had % successful return (debt, rate_mul) @external @view -@nonreentrant('lock') def debt(user: address) -> uint256: """ @notice Get the value of debt without changing the state @@ -440,7 +409,6 @@ def debt(user: address) -> uint256: @external @view -@nonreentrant('lock') def loan_exists(user: address) -> bool: """ @notice Check whether there is a loan of `user` in existence @@ -448,21 +416,26 @@ def loan_exists(user: address) -> bool: return self.loan[user].initial_debt > 0 -# No decorator because used in monetary policy @external @view +@reentrant def total_debt() -> uint256: """ @notice Total debt of this controller + @dev Marked as reentrant because used by monetary policy """ - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self._total_debt - return loan.initial_debt * rate_mul / loan.rate_mul + return self._get_total_debt() @internal @pure -def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint256: +def _get_y_effective( + collateral: uint256, + N: uint256, + discount: uint256, + _SQRT_BAND_RATIO: uint256, + _A: uint256, +) -> uint256: """ @notice Intermediary method which calculates y_effective defined as x_effective / p_base, however discounted by loan_discount. @@ -479,22 +452,34 @@ def get_y_effective(collateral: uint256, N: uint256, discount: uint256) -> uint2 # d_y_effective: uint256 = collateral * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N) # Make some extra discount to always deposit lower when we have DEAD_SHARES rounding d_y_effective: uint256 = unsafe_div( - collateral * unsafe_sub( - 10**18, min(discount + unsafe_div((DEAD_SHARES * 10**18), max(unsafe_div(collateral, N), DEAD_SHARES)), 10**18) + collateral + * unsafe_sub( + WAD, + min( + discount + + unsafe_div( + (DEAD_SHARES * WAD), + max(unsafe_div(collateral, N), DEAD_SHARES), + ), + WAD, + ), ), - unsafe_mul(SQRT_BAND_RATIO, N)) + unsafe_mul(_SQRT_BAND_RATIO, N), + ) y_effective: uint256 = d_y_effective - for i in range(1, MAX_TICKS_UINT): + for i: uint256 in range(1, MAX_TICKS_UINT): if i == N: break - d_y_effective = unsafe_div(d_y_effective * Aminus1, A) + d_y_effective = unsafe_div(d_y_effective * unsafe_sub(_A, 1), _A) y_effective = unsafe_add(y_effective, d_y_effective) return y_effective @internal @view -def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256: +def _calculate_debt_n1( + collateral: uint256, debt: uint256, N: uint256, user: address +) -> int256: """ @notice Calculate the upper band number for the deposit to sit in to support the given debt. Reverts if requested debt is too high. @@ -504,13 +489,19 @@ def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: add @return Upper band n1 (n1 <= n2) to deposit into. Signed integer """ assert debt > 0, "No loan" - n0: int256 = AMM.active_band() - p_base: uint256 = AMM.p_oracle_up(n0) + n0: int256 = staticcall AMM.active_band() + p_base: uint256 = staticcall AMM.p_oracle_up(n0) # x_effective = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k) # === d_y_effective * p_oracle_up(n1) * sum(...) === y_effective * p_oracle_up(n1) # d_y_effective = y / N / sqrt(A / (A - 1)) - y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, self.loan_discount + self.extra_health[user]) + y_effective: uint256 = self._get_y_effective( + collateral * COLLATERAL_PRECISION, + N, + self.loan_discount + self.extra_health[user], + SQRT_BAND_RATIO, + A, + ) # p_oracle_up(n1) = base_price * ((A - 1) / A)**n1 # We borrow up until min band touches p_oracle, @@ -520,116 +511,76 @@ def _calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: add # - we revert. # n1 is band number based on adiabatic trading, e.g. when p_oracle ~ p - y_effective = unsafe_div(y_effective * p_base, debt * BORROWED_PRECISION + 1) # Now it's a ratio + y_effective = unsafe_div( + y_effective * p_base, debt * BORROWED_PRECISION + 1 + ) # Now it's a ratio # n1 = floor(log(y_effective) / self.logAratio) # EVM semantics is not doing floor unlike Python, so we do this assert y_effective > 0, "Amount too low" - n1: int256 = self.wad_ln(y_effective) + n1: int256 = math._wad_ln(convert(y_effective, int256)) if n1 < 0: - n1 -= unsafe_sub(LOGN_A_RATIO, 1) # This is to deal with vyper's rounding of negative numbers + n1 -= unsafe_sub( + LOGN_A_RATIO, 1 + ) # This is to deal with vyper's rounding of negative numbers n1 = unsafe_div(n1, LOGN_A_RATIO) n1 = min(n1, 1024 - convert(N, int256)) + n0 if n1 <= n0: - assert AMM.can_skip_bands(n1 - 1), "Debt too high" + assert staticcall AMM.can_skip_bands(n1 - 1), "Debt too high" - # Let's not rely on active_band corresponding to price_oracle: - # this will be not correct if we are in the area of empty bands - assert AMM.p_oracle_up(n1) < AMM.price_oracle(), "Debt too high" + assert ( + staticcall AMM.p_oracle_up(n1) <= staticcall AMM.price_oracle() + ), "Debt too high" return n1 -@internal -@view -def max_p_base() -> uint256: - """ - @notice Calculate max base price including skipping bands - """ - p_oracle: uint256 = AMM.price_oracle() - # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands - n1: int256 = self.wad_ln(AMM.get_base_price() * 10**18 / p_oracle) - if n1 < 0: - n1 -= LOGN_A_RATIO - 1 # This is to deal with vyper's rounding of negative numbers - n1 = unsafe_div(n1, LOGN_A_RATIO) + MAX_P_BASE_BANDS - n_min: int256 = AMM.active_band_with_skip() - n1 = max(n1, n_min + 1) - p_base: uint256 = AMM.p_oracle_up(n1) - - for i in range(MAX_SKIP_TICKS + 1): - n1 -= 1 - if n1 <= n_min: - break - p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - @external @view -@nonreentrant('lock') -def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256 = 0, user: address = empty(address)) -> uint256: +def max_borrowable( + collateral: uint256, + N: uint256, + current_debt: uint256 = 0, + user: address = empty(address), +) -> uint256: """ @notice Calculation of maximum which can be borrowed (details in comments) @param collateral Collateral amount against which to borrow @param N number of bands to have the deposit into @param current_debt Current debt of the user (if any) @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Maximum amount of stablecoin to borrow - """ - # Calculation of maximum which can be borrowed. - # It corresponds to a minimum between the amount corresponding to price_oracle - # and the one given by the min reachable band. - # - # Given by p_oracle (perhaps needs to be multiplied by (A - 1) / A to account for mid-band effects) - # x_max ~= y_effective * p_oracle - # - # Given by band number: - # if n1 is the lowest empty band in the AMM - # xmax ~= y_effective * amm.p_oracle_up(n1) - # - # When n1 -= 1: - # p_oracle_up *= A / (A - 1) - # if N < MIN_TICKS or N > MAX_TICKS: - assert N >= MIN_TICKS_UINT and N <= MAX_TICKS_UINT - - y_effective: uint256 = self.get_y_effective(collateral * COLLATERAL_PRECISION, N, - self.loan_discount + self.extra_health[user]) - - x: uint256 = unsafe_sub(max(unsafe_div(y_effective * self.max_p_base(), 10**18), 1), 1) - x = unsafe_div(x * (10**18 - 10**14), unsafe_mul(10**18, BORROWED_PRECISION)) # Make it a bit smaller - return min(x, BORROWED_TOKEN.balanceOf(self) + current_debt) # Cannot borrow beyond the amount of coins Controller has + @return Maximum amount of borrowed asset to borrow + """ + return staticcall self._view.max_borrowable( + collateral, N, current_debt, user + ) +# TODO use sub_or_zero everywhere @external @view -@nonreentrant('lock') -def min_collateral(debt: uint256, N: uint256, user: address = empty(address)) -> uint256: +def min_collateral( + debt: uint256, N: uint256, user: address = empty(address) +) -> uint256: """ - @notice Minimal amount of collateral required to support debt - @param debt The debt to support - @param N Number of bands to deposit into + @notice Calculation of minimum collaterl amount required for given debt amount + @param debt The amount of debt for which calculation should be done + @param N number of bands to have the deposit into @param user User to calculate the value for (only necessary for nonzero extra_health) - @return Minimal collateral required + @return Minimum amount of collateral asset to provide """ - # Add N**2 to account for precision loss in multiple bands, e.g. N / (y/N) = N**2 / y - assert N <= MAX_TICKS_UINT and N >= MIN_TICKS_UINT - return unsafe_div( - unsafe_div( - debt * unsafe_mul(10**18, BORROWED_PRECISION) / self.max_p_base() * 10**18 / self.get_y_effective(10**18, N, self.loan_discount + self.extra_health[user]) + unsafe_add(unsafe_mul(N, unsafe_add(N, 2 * DEAD_SHARES)), unsafe_sub(COLLATERAL_PRECISION, 1)), - COLLATERAL_PRECISION - ) * 10**18, - 10**18 - 10**14) + return staticcall self._view.min_collateral(debt, N, user) @external @view -@nonreentrant('lock') -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address = empty(address)) -> int256: +def calculate_debt_n1( + collateral: uint256, + debt: uint256, + N: uint256, + user: address = empty(address), +) -> int256: """ @notice Calculate the upper band number for the deposit to sit in to support the given debt. Reverts if requested debt is too high. @@ -643,135 +594,140 @@ def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: addr @internal -def transferFrom(token: ERC20, _from: address, _to: address, amount: uint256): - if amount > 0: - assert token.transferFrom(_from, _to, amount, default_return_value=True) - - -@internal -def transfer(token: ERC20, _to: address, amount: uint256): - if amount > 0: - assert token.transfer(_to, amount, default_return_value=True) +@view +def _check_loan_exists(debt: uint256): + # Abstraction to save bytecode on error messages + assert debt > 0, "Loan doesn't exist" @internal -def execute_callback(callbacker: address, callback_sig: bytes4, - user: address, stablecoins: uint256, collateral: uint256, debt: uint256, - callback_args: DynArray[uint256, 5], callback_bytes: Bytes[10**4]) -> CallbackData: +def execute_callback( + callbacker: address, + callback_sig: bytes4, + user: address, + borrowed: uint256, + collateral: uint256, + debt: uint256, + calldata: Bytes[CALLDATA_MAX_SIZE], +) -> IController.CallbackData: assert callbacker != COLLATERAL_TOKEN.address assert callbacker != BORROWED_TOKEN.address - data: CallbackData = empty(CallbackData) - data.active_band = AMM.active_band() - band_x: uint256 = AMM.bands_x(data.active_band) - band_y: uint256 = AMM.bands_y(data.active_band) + data: IController.CallbackData = empty(IController.CallbackData) + data.active_band = staticcall AMM.active_band() + band_x: uint256 = staticcall AMM.bands_x(data.active_band) + band_y: uint256 = staticcall AMM.bands_y(data.active_band) # Callback response: Bytes[64] = raw_call( callbacker, - concat(callback_sig, _abi_encode(user, stablecoins, collateral, debt, callback_args, callback_bytes)), - max_outsize=64 + concat( + callback_sig, + abi_encode(user, borrowed, collateral, debt, calldata), + ), + max_outsize=64, ) - data.stablecoins = convert(slice(response, 0, 32), uint256) + data.borrowed = convert(slice(response, 0, 32), uint256) data.collateral = convert(slice(response, 32, 32), uint256) # Checks after callback - assert data.active_band == AMM.active_band() - assert band_x == AMM.bands_x(data.active_band) - assert band_y == AMM.bands_y(data.active_band) + assert data.active_band == staticcall AMM.active_band() + assert band_x == staticcall AMM.bands_x(data.active_band) + assert band_y == staticcall AMM.bands_y(data.active_band) return data -@internal -def _create_loan(collateral: uint256, debt: uint256, N: uint256, transfer_coins: bool, _for: address): - assert self.loan[_for].initial_debt == 0, "Loan already created" - assert N > MIN_TICKS-1, "Need more ticks" - assert N < MAX_TICKS+1, "Need less ticks" - - n1: int256 = self._calculate_debt_n1(collateral, debt, N, _for) - n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - - rate_mul: uint256 = AMM.get_rate_mul() - self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul}) - liquidation_discount: uint256 = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount - - n_loans: uint256 = self.n_loans - self.loans[n_loans] = _for - self.loan_ix[_for] = n_loans - self.n_loans = unsafe_add(n_loans, 1) - - self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + debt - self._total_debt.rate_mul = rate_mul - - AMM.deposit_range(_for, collateral, n1, n2) - self.minted += debt - - if transfer_coins: - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self.transfer(BORROWED_TOKEN, _for, debt) - - self._save_rate() - - log UserState(_for, collateral, debt, n1, n2, liquidation_discount) - log Borrow(_for, collateral, debt) - +# TODO use underscore for args everywhere @external -@nonreentrant('lock') -def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address = msg.sender): - """ - @notice Create loan +def create_loan( + collateral: uint256, + debt: uint256, + N: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", +): + """ + @notice Create loan but pass borrowed tokens to a callback first so that it can build leverage @param collateral Amount of collateral to use - @param debt Stablecoin debt to take + @param debt Borrowed asset debt to take @param N Number of bands to deposit into (to do autoliquidation-deliquidation), can be from MIN_TICKS to MAX_TICKS @param _for Address to create the loan for + @param callbacker Address of the callback contract + @param calldata Any data for callbacker """ if _for != tx.origin: # We can create a loan for tx.origin (for example when wrapping ETH with EOA), # however need to approve in other cases assert self._check_approval(_for) - self._create_loan(collateral, debt, N, True, _for) + more_collateral: uint256 = 0 + if callbacker != empty(address): + tkn.transfer(BORROWED_TOKEN, callbacker, debt) + # If there is any unused debt, callbacker can send it to the user + more_collateral = self.execute_callback( + callbacker, + CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral + + total_collateral: uint256 = collateral + more_collateral -@external -@nonreentrant('lock') -def create_loan_extended(collateral: uint256, debt: uint256, N: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): - """ - @notice Create loan but pass stablecoin to a callback first so that it can build leverage - @param collateral Amount of collateral to use - @param debt Stablecoin debt to take - @param N Number of bands to deposit into (to do autoliquidation-deliquidation), - can be from MIN_TICKS to MAX_TICKS - @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc - @param _for Address to create the loan for - """ - if _for != tx.origin: - assert self._check_approval(_for) + assert self.loan[_for].initial_debt == 0, "Loan already created" + assert N > MIN_TICKS_UINT - 1, "Need more ticks" + assert N < MAX_TICKS_UINT + 1, "Need less ticks" - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) + n1: int256 = self._calculate_debt_n1(total_collateral, debt, N, _for) + n2: int256 = n1 + convert(unsafe_sub(N, 1), int256) - # For compatibility - callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES - if callback_bytes == b"": - callback_sig = CALLBACK_DEPOSIT - # Callback - # If there is any unused debt, callbacker can send it to the user - more_collateral: uint256 = self.execute_callback( - callbacker, callback_sig, _for, 0, collateral, debt, callback_args, callback_bytes).collateral + rate_mul: uint256 = staticcall AMM.get_rate_mul() + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + liquidation_discount: uint256 = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount - # After callback - self._create_loan(collateral + more_collateral, debt, N, False, _for) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + n_loans: uint256 = self.n_loans + self.loans[n_loans] = _for + self.loan_ix[_for] = n_loans + self.n_loans = unsafe_add(n_loans, 1) + + extcall AMM.deposit_range(_for, total_collateral, n1, n2) + + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + tkn.transfer(BORROWED_TOKEN, _for, debt) + + self._update_total_debt(debt, rate_mul, True) + self.lent += debt + self._save_rate() + + log IController.UserState( + user=_for, + collateral=total_collateral, + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) + log IController.Borrow( + user=_for, collateral_increase=total_collateral, loan_increase=debt + ) @internal -def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address, remove_collateral: bool, - check_rounding: bool): +def _add_collateral_borrow( + d_collateral: uint256, + d_debt: uint256, + _for: address, + remove_collateral: bool, + check_rounding: bool, +) -> uint256: """ @notice Internal method to borrow and add or remove collateral @param d_collateral Amount of collateral to add @@ -783,13 +739,11 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address debt: uint256 = 0 rate_mul: uint256 = 0 debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" + self._check_loan_exists(debt) debt += d_debt - ns: int256[2] = AMM.read_user_tick_numbers(_for) - size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) - xy: uint256[2] = AMM.withdraw(_for, 10**18) - assert xy[0] == 0, "Already in underwater mode" + xy: uint256[2] = extcall AMM.withdraw(_for, WAD) + assert xy[0] == 0, "Underwater" if remove_collateral: xy[1] -= d_collateral else: @@ -799,12 +753,17 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address # This check is only needed when we add collateral for someone else, so gas is not an issue # 2 * 10**(18 - borrow_decimals + collateral_decimals) = # = 2 * 10**18 * 10**(18 - borrow_decimals) / 10**(collateral_decimals) - assert d_collateral * AMM.price_oracle() > 2 * 10**18 * BORROWED_PRECISION / COLLATERAL_PRECISION + assert ( + d_collateral * staticcall AMM.price_oracle() + > 2 * WAD * BORROWED_PRECISION // COLLATERAL_PRECISION + ) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + size: uint256 = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) n1: int256 = self._calculate_debt_n1(xy[1], debt, size, _for) n2: int256 = n1 + unsafe_sub(ns[1], ns[0]) - AMM.deposit_range(_for, xy[1], n1, n2) - self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul}) + extcall AMM.deposit_range(_for, xy[1], n1, n2) + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) liquidation_discount: uint256 = 0 if _for == msg.sender: @@ -813,20 +772,28 @@ def _add_collateral_borrow(d_collateral: uint256, d_debt: uint256, _for: address else: liquidation_discount = self.liquidation_discounts[_for] - if d_debt != 0: - self._total_debt.initial_debt = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul + d_debt - self._total_debt.rate_mul = rate_mul - if remove_collateral: - log RemoveCollateral(_for, d_collateral) + log IController.RemoveCollateral( + user=_for, collateral_decrease=d_collateral + ) else: - log Borrow(_for, d_collateral, d_debt) + log IController.Borrow( + user=_for, collateral_increase=d_collateral, loan_increase=d_debt + ) + + log IController.UserState( + user=_for, + collateral=xy[1], + debt=debt, + n1=n1, + n2=n2, + liquidation_discount=liquidation_discount, + ) - log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) + return rate_mul @external -@nonreentrant('lock') def add_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Add extra collateral to avoid bad liqidations @@ -836,12 +803,11 @@ def add_collateral(collateral: uint256, _for: address = msg.sender): if collateral == 0: return self._add_collateral_borrow(collateral, 0, _for, False, _for != msg.sender) - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) self._save_rate() @external -@nonreentrant('lock') def remove_collateral(collateral: uint256, _for: address = msg.sender): """ @notice Remove some collateral without repaying the debt @@ -852,61 +818,55 @@ def remove_collateral(collateral: uint256, _for: address = msg.sender): return assert self._check_approval(_for) self._add_collateral_borrow(collateral, 0, _for, True, False) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, collateral) self._save_rate() @external -@nonreentrant('lock') -def borrow_more(collateral: uint256, debt: uint256, _for: address = msg.sender): - """ - @notice Borrow more stablecoins while adding more collateral (not necessary) +def borrow_more( + collateral: uint256, + debt: uint256, + _for: address = msg.sender, + callbacker: address = empty(address), + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", +): + """ + @notice Borrow more borrowed tokens while adding more collateral using a callback (to leverage more) @param collateral Amount of collateral to add - @param debt Amount of stablecoin debt to take + @param debt Amount of borrowed asset debt to take @param _for Address to borrow for - """ - if debt == 0: - return - assert self._check_approval(_for) - self._add_collateral_borrow(collateral, debt, _for, False, False) - self.minted += debt - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self.transfer(BORROWED_TOKEN, _for, debt) - self._save_rate() - - -@external -@nonreentrant('lock') -def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): - """ - @notice Borrow more stablecoins while adding more collateral using a callback (to leverage more) - @param collateral Amount of collateral to add - @param debt Amount of stablecoin debt to take @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc - @param _for Address to borrow for + @param calldata Any data for callbacker """ if debt == 0: return assert self._check_approval(_for) - # Before callback - self.transfer(BORROWED_TOKEN, callbacker, debt) + more_collateral: uint256 = 0 + if callbacker != empty(address): + tkn.transfer(BORROWED_TOKEN, callbacker, debt) + # If there is any unused debt, callbacker can send it to the user + more_collateral = self.execute_callback( + callbacker, + CALLBACK_DEPOSIT, + _for, + 0, + collateral, + debt, + calldata, + ).collateral + + rate_mul: uint256 = self._add_collateral_borrow( + collateral + more_collateral, debt, _for, False, False + ) - # For compatibility - callback_sig: bytes4 = CALLBACK_DEPOSIT_WITH_BYTES - if callback_bytes == b"": - callback_sig = CALLBACK_DEPOSIT - # Callback - # If there is any unused debt, callbacker can send it to the user - more_collateral: uint256 = self.execute_callback( - callbacker, callback_sig, _for, 0, collateral, debt, callback_args, callback_bytes).collateral - - # After callback - self._add_collateral_borrow(collateral + more_collateral, debt, _for, False, False) - self.minted += debt - self.transferFrom(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + tkn.transfer_from(COLLATERAL_TOKEN, msg.sender, AMM.address, collateral) + tkn.transfer_from(COLLATERAL_TOKEN, callbacker, AMM.address, more_collateral) + if callbacker == empty(address): + tkn.transfer(BORROWED_TOKEN, _for, debt) + + self._update_total_debt(debt, rate_mul, True) + self.lent += debt self._save_rate() @@ -914,7 +874,9 @@ def borrow_more_extended(collateral: uint256, debt: uint256, callbacker: address def _remove_from_list(_for: address): last_loan_ix: uint256 = self.n_loans - 1 loan_ix: uint256 = self.loan_ix[_for] - assert self.loans[loan_ix] == _for # dev: should never fail but safety first + assert ( + self.loans[loan_ix] == _for + ) # dev: should never fail but safety first self.loan_ix[_for] = 0 if loan_ix < last_loan_ix: # Need to replace last_loan: address = self.loans[last_loan_ix] @@ -923,169 +885,223 @@ def _remove_from_list(_for: address): self.n_loans = last_loan_ix -@external -@nonreentrant('lock') -def repay(_d_debt: uint256, _for: address = msg.sender, max_active_band: int256 = 2**255-1): - """ - @notice Repay debt (partially or fully) - @param _d_debt The amount of debt to repay. If higher than the current debt - will do full repayment - @param _for The user to repay the debt for - @param max_active_band Don't allow active band to be higher than this (to prevent front-running the repay) - """ - if _d_debt == 0: - return - # Or repay all for MAX_UINT256 - # Withdraw if debt become 0 - debt: uint256 = 0 - rate_mul: uint256 = 0 - debt, rate_mul = self._debt(_for) - assert debt > 0, "Loan doesn't exist" - d_debt: uint256 = min(debt, _d_debt) - debt = unsafe_sub(debt, d_debt) - approval: bool = self._check_approval(_for) +@internal +def _repay_full( + _for: address, + _debt: uint256, # same as _d_debt in this case + _approval: bool, + _xy: uint256[2], + _cb: IController.CallbackData, + _callbacker: address, +): + if _callbacker == empty(address): + _xy = extcall AMM.withdraw(_for, WAD) + + # ================= Recover borrowed tokens (xy[0]) ================= + non_wallet_d_debt: uint256 = _xy[0] + _cb.borrowed + wallet_d_debt: uint256 = unsafe_sub(max(_debt, non_wallet_d_debt), non_wallet_d_debt) + if _xy[0] > 0: # pull borrowed tokens from AMM (already soft liquidated) + assert _approval + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, _xy[0]) + tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, wallet_d_debt) + tkn.transfer(BORROWED_TOKEN, _for, unsafe_sub(max(non_wallet_d_debt, _debt), _debt)) + + + # ================= Recover collateral tokens (xy[1]) ================= + if _callbacker == empty(address): + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, _for, _xy[1]) + else: + tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, _for, _cb.collateral) - if debt == 0: - # Allow to withdraw all assets even when underwater - xy: uint256[2] = AMM.withdraw(_for, 10**18) - if xy[0] > 0: - # Only allow full repayment when underwater for the sender to do - assert approval - self.transferFrom(BORROWED_TOKEN, AMM.address, _for, xy[0]) - if xy[1] > 0: - self.transferFrom(COLLATERAL_TOKEN, AMM.address, _for, xy[1]) - log UserState(_for, 0, 0, 0, 0, 0) - log Repay(_for, xy[1], d_debt) - self._remove_from_list(_for) + self._remove_from_list(_for) - else: - active_band: int256 = AMM.active_band_with_skip() - assert active_band <= max_active_band - - ns: int256[2] = AMM.read_user_tick_numbers(_for) - size: int256 = unsafe_sub(ns[1], ns[0]) - liquidation_discount: uint256 = self.liquidation_discounts[_for] - - if ns[0] > active_band: - # Not in liquidation - can move bands - xy: uint256[2] = AMM.withdraw(_for, 10**18) - n1: int256 = self._calculate_debt_n1(xy[1], debt, convert(unsafe_add(size, 1), uint256), _for) - n2: int256 = n1 + size - AMM.deposit_range(_for, xy[1], n1, n2) - if approval: - # Update liquidation discount only if we are that same user. No rugs - liquidation_discount = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount - log UserState(_for, xy[1], debt, n1, n2, liquidation_discount) - log Repay(_for, 0, d_debt) - else: - # Underwater - cannot move band but can avoid a bad liquidation - log UserState(_for, max_value(uint256), debt, ns[0], ns[1], liquidation_discount) - log Repay(_for, 0, d_debt) + log IController.UserState( + user=_for, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0 + ) + log IController.Repay( + user=_for, collateral_decrease=_xy[1], loan_decrease=_debt + ) - if not approval: - # Doesn't allow non-sender to repay in a way which ends with unhealthy state - # full = False to make this condition non-manipulatable (and also cheaper on gas) - assert self._health(_for, debt, False, liquidation_discount) > 0 - # If we withdrew already - will burn less! - self.transferFrom(BORROWED_TOKEN, msg.sender, self, d_debt) # fail: insufficient funds - self.redeemed += d_debt +@internal +def _repay_partial( + _for: address, + _debt: uint256, + _wallet_d_debt: uint256, + _approval: bool, + _xy: uint256[2], + _cb: IController.CallbackData, + _callbacker: address, + _max_active_band: int256, + _shrink: bool, +) -> uint256: + # slippage-like check to prevent dos on repay (grief attack) + active_band: int256 = staticcall AMM.active_band_with_skip() + new_collateral: uint256 = _xy[1] + if _callbacker != empty(address): + active_band = _cb.active_band + new_collateral = _cb.collateral + assert active_band <= _max_active_band + + ns: int256[2] = staticcall AMM.read_user_tick_numbers(_for) + size: int256 = unsafe_sub(ns[1], ns[0]) + if ns[0] <= active_band and _shrink: + assert ns[1] >= active_band + MIN_TICKS, "Can't shrink" + size = unsafe_sub(ns[1], active_band + 1) + liquidation_discount: uint256 = self.liquidation_discounts[_for] + # _debt > _wallet_d_debt + cb.borrowed + xy[0] (check repay method) + new_debt: uint256 = unsafe_sub(_debt, unsafe_add(_cb.borrowed, _wallet_d_debt)) + + if ns[0] > active_band or _shrink: + # Not underwater or shrink mode - can move bands + if _callbacker == empty(address): + _xy = extcall AMM.withdraw(_for, WAD) + new_collateral = _xy[1] + new_debt -=_xy[0] + + ns[0] = self._calculate_debt_n1( + new_collateral, + new_debt, + convert(unsafe_add(size, 1), uint256), + _for, + ) + ns[1] = ns[0] + size + extcall AMM.deposit_range(_for, new_collateral, ns[0], ns[1]) + else: + # Underwater without shrink - cannot use callback or move bands. + # But can avoid a bad liquidation just reducing debt amount. + assert _callbacker == empty(address) - self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul}) - total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul - self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt) - self._total_debt.rate_mul = rate_mul + if _approval: + # Update liquidation discount only if we are that same user. No rugs + liquidation_discount = self.liquidation_discount + self.liquidation_discounts[_for] = liquidation_discount + else: + # Doesn't allow non-sender to repay in a way which ends with unhealthy state + # full = False to make this condition non-manipulatable (and also cheaper on gas) + assert self._health(_for, new_debt, False, liquidation_discount) > 0 + + # ================= Recover borrowed tokens (xy[0]) ================= + if _shrink: + assert _approval + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, _xy[0]) + tkn.transfer_from(BORROWED_TOKEN, _callbacker, self, _cb.borrowed) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, _wallet_d_debt) + + # ================= Recover collateral tokens (xy[1]) ================= + tkn.transfer_from(COLLATERAL_TOKEN, _callbacker, AMM.address, _cb.collateral) + + d_debt: uint256 = _debt - new_debt + + log IController.UserState( + user=_for, + collateral=new_collateral, + debt=new_debt, + n1=ns[0], + n2=ns[1], + liquidation_discount=liquidation_discount, + ) + log IController.Repay( + user=_for, + collateral_decrease=unsafe_sub(max(_xy[1], new_collateral), new_collateral), + loan_decrease=d_debt, + ) - self._save_rate() + return d_debt @external -@nonreentrant('lock') -def repay_extended(callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b"", _for: address = msg.sender): +def repay( + _wallet_d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[CALLDATA_MAX_SIZE] = b"", + shrink: bool = False +): """ - @notice Repay loan but get a stablecoin for that from callback (to deleverage) + @notice Repay debt (partially or fully) + @param _wallet_d_debt The amount of debt to repay from user's wallet. + If it's max_value(uint256) or just higher than the current debt - will do full repayment. + @param _for The user to repay the debt for + @param max_active_band Don't allow active band to be higher than this (to prevent front-running the repay) @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc - @param _for Address to repay for + @param calldata Any data for callbacker + @param shrink Whether shrink soft-liquidated part of the position or not """ - assert self._check_approval(_for) - - # Before callback - ns: int256[2] = AMM.read_user_tick_numbers(_for) - xy: uint256[2] = AMM.withdraw(_for, 10**18) debt: uint256 = 0 rate_mul: uint256 = 0 debt, rate_mul = self._debt(_for) - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - - # For compatibility - callback_sig: bytes4 = CALLBACK_REPAY_WITH_BYTES - if callback_bytes == b"": - callback_sig = CALLBACK_REPAY - cb: CallbackData = self.execute_callback( - callbacker, callback_sig, _for, xy[0], xy[1], debt, callback_args, callback_bytes) - - # After callback - total_stablecoins: uint256 = cb.stablecoins + xy[0] - assert total_stablecoins > 0 # dev: no coins to repay - - # d_debt: uint256 = min(debt, total_stablecoins) - - d_debt: uint256 = 0 - - # If we have more stablecoins than the debt - full repayment and closing the position - if total_stablecoins >= debt: - d_debt = debt - debt = 0 - self._remove_from_list(_for) - - # Transfer debt to self, everything else to _for - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - self.transferFrom(BORROWED_TOKEN, AMM.address, self, xy[0]) - if total_stablecoins > d_debt: - self.transfer(BORROWED_TOKEN, _for, unsafe_sub(total_stablecoins, d_debt)) - self.transferFrom(COLLATERAL_TOKEN, callbacker, _for, cb.collateral) - - log UserState(_for, 0, 0, 0, 0, 0) - - # Else - partial repayment -> deleverage, but only if we are not underwater + self._check_loan_exists(debt) + approval: bool = self._check_approval(_for) + xy: uint256[2] = staticcall AMM.get_sum_xy(_for) + + cb: IController.CallbackData = empty(IController.CallbackData) + if callbacker != empty(address): + assert approval # dev: need approval for callback + xy = extcall AMM.withdraw(_for, WAD) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) + cb = self.execute_callback( + callbacker, CALLBACK_REPAY, _for, xy[0], xy[1], debt, calldata + ) + + d_debt: uint256 = min(min(_wallet_d_debt, debt) + xy[0] + cb.borrowed, debt) + assert d_debt > 0 # dev: no coins to repay + + if d_debt == debt: + self._repay_full(_for, d_debt, approval, xy, cb, callbacker) else: - size: int256 = unsafe_sub(ns[1], ns[0]) - assert ns[0] > cb.active_band - d_debt = cb.stablecoins # cb.stablecoins <= total_stablecoins < debt - debt = unsafe_sub(debt, cb.stablecoins) - - # Not in liquidation - can move bands - n1: int256 = self._calculate_debt_n1(cb.collateral, debt, convert(unsafe_add(size, 1), uint256), _for) - n2: int256 = n1 + size - AMM.deposit_range(_for, cb.collateral, n1, n2) - liquidation_discount: uint256 = self.liquidation_discount - self.liquidation_discounts[_for] = liquidation_discount + d_debt = self._repay_partial( + _for, + debt, + _wallet_d_debt, + approval, + xy, + cb, + callbacker, + max_active_band, + shrink, + ) + debt -= d_debt + + self.loan[_for] = IController.Loan(initial_debt=debt, rate_mul=rate_mul) + self._update_total_debt(d_debt, rate_mul, False) + self.repaid += d_debt + self._save_rate() - self.transferFrom(COLLATERAL_TOKEN, callbacker, AMM.address, cb.collateral) - # Stablecoin is all spent to repay debt -> all goes to self - self.transferFrom(BORROWED_TOKEN, callbacker, self, cb.stablecoins) - # We are above active band, so xy[0] is 0 anyway - log UserState(_for, cb.collateral, debt, n1, n2, liquidation_discount) - xy[1] -= cb.collateral +@view +@external +def tokens_to_shrink(user: address) -> uint256: + """ + @notice Calculate the amount of borrowed asset required to shrink the user's position + @param user Address of the user to shrink the position for + @return The amount of borrowed asset needed + """ + active_band: int256 = staticcall AMM.active_band_with_skip() + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) - # No need to check _health() because it's the _for + if ns[0] > active_band: + return 0 - # Common calls which we will do regardless of whether it's a full repay or not - log Repay(_for, xy[1], d_debt) - self.redeemed += d_debt - self.loan[_for] = Loan({initial_debt: debt, rate_mul: rate_mul}) - total_debt: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul - self._total_debt.initial_debt = unsafe_sub(max(total_debt, d_debt), d_debt) - self._total_debt.rate_mul = rate_mul + assert ns[1] >= active_band + MIN_TICKS, "Can't shrink" + size: int256 = unsafe_sub(ns[1], active_band + 1) + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + current_debt: uint256 = self._debt(user)[0] + new_debt: uint256 = unsafe_sub(max(current_debt, xy[0]), xy[0]) + max_borrowable: uint256 = staticcall self._view.max_borrowable( + xy[1], convert(unsafe_add(size, 1), uint256), new_debt, user + ) - self._save_rate() + return unsafe_sub(max(new_debt, max_borrowable), max_borrowable) @internal @view -def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint256) -> int256: +def _health( + user: address, debt: uint256, full: bool, liquidation_discount: uint256 +) -> int256: """ @notice Returns position health normalized to 1e18 for the user. Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation @@ -1095,25 +1111,43 @@ def _health(user: address, debt: uint256, full: bool, liquidation_discount: uint @param liquidation_discount Liquidation discount to use (can be 0) @return Health: > 0 = good. """ - assert debt > 0, "Loan doesn't exist" - health: int256 = 10**18 - convert(liquidation_discount, int256) - health = unsafe_div(convert(AMM.get_x_down(user), int256) * health, convert(debt, int256)) - 10**18 + self._check_loan_exists(debt) + health: int256 = SWAD - convert(liquidation_discount, int256) + health = ( + unsafe_div( + convert(staticcall AMM.get_x_down(user), int256) * health, + convert(debt, int256), + ) + - SWAD + ) if full: - ns0: int256 = AMM.read_user_tick_numbers(user)[0] # ns[1] > ns[0] - if ns0 > AMM.active_band(): # We are not in liquidation mode - p: uint256 = AMM.price_oracle() - p_up: uint256 = AMM.p_oracle_up(ns0) + ns0: int256 = (staticcall AMM.read_user_tick_numbers(user))[0] # ns[1] > ns[0] + if ns0 > staticcall AMM.active_band(): # We are not in liquidation mode + p: uint256 = staticcall AMM.price_oracle() + p_up: uint256 = staticcall AMM.p_oracle_up(ns0) if p > p_up: - health += convert(unsafe_div(unsafe_sub(p, p_up) * AMM.get_sum_xy(user)[1] * COLLATERAL_PRECISION, debt * BORROWED_PRECISION), int256) - + health += convert( + unsafe_div( + unsafe_sub(p, p_up) + * (staticcall AMM.get_sum_xy(user))[1] + * COLLATERAL_PRECISION, + debt * BORROWED_PRECISION, + ), + int256, + ) return health @external @view -@nonreentrant('lock') -def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256 = 0) -> int256: +def health_calculator( + user: address, + d_collateral: int256, + d_debt: int256, + full: bool, + N: uint256 = 0, +) -> int256: """ @notice Health predictor in case user changes the debt or collateral @param user Address of the user @@ -1123,211 +1157,185 @@ def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: @param N Number of bands in case loan doesn't yet exist @return Signed health value """ - ns: int256[2] = AMM.read_user_tick_numbers(user) - debt: int256 = convert(self._debt(user)[0], int256) - n: uint256 = N - ld: int256 = 0 - if debt != 0: - ld = convert(self.liquidation_discounts[user], int256) - n = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) - else: - ld = convert(self.liquidation_discount, int256) - ns[0] = max_value(int256) # This will trigger a "re-deposit" - - n1: int256 = 0 - collateral: int256 = 0 - x_eff: int256 = 0 - debt += d_debt - assert debt > 0, "Non-positive debt" - - active_band: int256 = AMM.active_band_with_skip() - - if ns[0] > active_band: # re-deposit - collateral = convert(AMM.get_sum_xy(user)[1], int256) + d_collateral - n1 = self._calculate_debt_n1(convert(collateral, uint256), convert(debt, uint256), n, user) - collateral *= convert(COLLATERAL_PRECISION, int256) # now has 18 decimals - else: - n1 = ns[0] - x_eff = convert(AMM.get_x_down(user) * unsafe_mul(10**18, BORROWED_PRECISION), int256) - - debt *= convert(BORROWED_PRECISION, int256) - - p0: int256 = convert(AMM.p_oracle_up(n1), int256) - if ns[0] > active_band: - x_eff = convert(self.get_y_effective(convert(collateral, uint256), n, 0), int256) * p0 - - health: int256 = unsafe_div(x_eff, debt) - health = health - unsafe_div(health * ld, 10**18) - 10**18 - - if full: - if n1 > active_band: # We are not in liquidation mode - p_diff: int256 = max(p0, convert(AMM.price_oracle(), int256)) - p0 - if p_diff > 0: - health += unsafe_div(p_diff * collateral, debt) - - return health + return staticcall self._view.health_calculator( + user, d_collateral, d_debt, full, N + ) @internal @pure def _get_f_remove(frac: uint256, health_limit: uint256) -> uint256: # f_remove = ((1 + h / 2) / (1 + h) * (1 - frac) + frac) * frac - f_remove: uint256 = 10 ** 18 - if frac < 10 ** 18: - f_remove = unsafe_div(unsafe_mul(unsafe_add(10 ** 18, unsafe_div(health_limit, 2)), unsafe_sub(10 ** 18, frac)), unsafe_add(10 ** 18, health_limit)) - f_remove = unsafe_div(unsafe_mul(unsafe_add(f_remove, frac), frac), 10 ** 18) + f_remove: uint256 = WAD + if frac < WAD: + f_remove = unsafe_div( + unsafe_mul( + unsafe_add(WAD, unsafe_div(health_limit, 2)), + unsafe_sub(WAD, frac), + ), + unsafe_add(WAD, health_limit), + ) + f_remove = unsafe_div(unsafe_mul(unsafe_add(f_remove, frac), frac), WAD) return f_remove -@internal -def _liquidate(user: address, min_x: uint256, health_limit: uint256, frac: uint256, - callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""): + +@external +def liquidate( + user: address, + min_x: uint256, + _frac: uint256 = 10**18, + callbacker: address = empty(address), + calldata: Bytes[10**4] = b"", +): """ - @notice Perform a bad liquidation of user if the health is too bad - @param user Address of the user - @param min_x Minimal amount of stablecoin withdrawn (to avoid liquidators being sandwiched) - @param health_limit Minimal health to liquidate at - @param frac Fraction to liquidate; 100% = 10**18 + @notice Perform a bad liquidation (or self-liquidation) of user if health is not good + @param min_x Minimal amount of borrowed asset to receive (to avoid liquidators being sandwiched) + @param _frac Fraction to liquidate; 100% = 10**18 @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc + @param calldata Any data for callbacker """ + health_limit: uint256 = 0 + if not self._check_approval(user): + health_limit = self.liquidation_discounts[user] debt: uint256 = 0 rate_mul: uint256 = 0 debt, rate_mul = self._debt(user) if health_limit != 0: - assert self._health(user, debt, True, health_limit) < 0, "Not enough rekt" + assert ( + self._health(user, debt, True, health_limit) < 0 + ), "Not enough rekt" final_debt: uint256 = debt - debt = unsafe_div(debt * frac + (10**18 - 1), 10**18) + assert _frac <= WAD, "frac>100%" + debt = unsafe_div(debt * _frac + (WAD - 1), WAD) assert debt > 0 final_debt = unsafe_sub(final_debt, debt) - # Withdraw sender's stablecoin and collateral to our contract + # Withdraw sender's borrowed and collateral to our contract # When frac is set - we withdraw a bit less for the same debt fraction # f_remove = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac # where h is health limit. # This is less than full h discount but more than no discount - xy: uint256[2] = AMM.withdraw(user, self._get_f_remove(frac, health_limit)) # [stable, collateral] + xy: uint256[2] = extcall AMM.withdraw( + user, self._get_f_remove(_frac, health_limit) + ) # [stable, collateral] # x increase in same block -> price up -> good # x decrease in same block -> price down -> bad assert xy[0] >= min_x, "Slippage" min_amm_burn: uint256 = min(xy[0], debt) - self.transferFrom(BORROWED_TOKEN, AMM.address, self, min_amm_burn) + tkn.transfer_from(BORROWED_TOKEN, AMM.address, self, min_amm_burn) if debt > xy[0]: to_repay: uint256 = unsafe_sub(debt, xy[0]) if callbacker == empty(address): # Withdraw collateral if no callback is present - self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) # Request what's left from user - self.transferFrom(BORROWED_TOKEN, msg.sender, self, to_repay) + tkn.transfer_from(BORROWED_TOKEN, msg.sender, self, to_repay) else: # Move collateral to callbacker, call it and remove everything from it back in - self.transferFrom(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) - # For compatibility - callback_sig: bytes4 = CALLBACK_LIQUIDATE_WITH_BYTES - if callback_bytes == b"": - callback_sig = CALLBACK_LIQUIDATE + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, callbacker, xy[1]) # Callback - cb: CallbackData = self.execute_callback( - callbacker, callback_sig, user, xy[0], xy[1], debt, callback_args, callback_bytes) - assert cb.stablecoins >= to_repay, "not enough proceeds" - if cb.stablecoins > to_repay: - self.transferFrom(BORROWED_TOKEN, callbacker, msg.sender, unsafe_sub(cb.stablecoins, to_repay)) - self.transferFrom(BORROWED_TOKEN, callbacker, self, to_repay) - self.transferFrom(COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral) - + cb: IController.CallbackData = self.execute_callback( + callbacker, + CALLBACK_LIQUIDATE, + user, + xy[0], + xy[1], + debt, + calldata, + ) + assert cb.borrowed >= to_repay, "no enough proceeds" + if cb.borrowed > to_repay: + tkn.transfer_from( + BORROWED_TOKEN, + callbacker, + msg.sender, + unsafe_sub(cb.borrowed, to_repay), + ) + tkn.transfer_from(BORROWED_TOKEN, callbacker, self, to_repay) + tkn.transfer_from( + COLLATERAL_TOKEN, callbacker, msg.sender, cb.collateral + ) else: # Withdraw collateral - self.transferFrom(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) + tkn.transfer_from(COLLATERAL_TOKEN, AMM.address, msg.sender, xy[1]) # Return what's left to user if xy[0] > debt: - self.transferFrom(BORROWED_TOKEN, AMM.address, msg.sender, unsafe_sub(xy[0], debt)) + tkn.transfer_from( + BORROWED_TOKEN, + AMM.address, + msg.sender, + unsafe_sub(xy[0], debt), + ) + + self.loan[user] = IController.Loan(initial_debt=final_debt, rate_mul=rate_mul) + self._update_total_debt(debt, rate_mul, False) + self.repaid += debt + self._save_rate() - self.redeemed += debt - self.loan[user] = Loan({initial_debt: final_debt, rate_mul: rate_mul}) - log Repay(user, xy[1], debt) - log Liquidate(msg.sender, user, xy[1], xy[0], debt) + log IController.Repay( + user=user, collateral_decrease=xy[1], loan_decrease=debt + ) + log IController.Liquidate( + liquidator=msg.sender, + user=user, + collateral_received=xy[1], + borrowed_received=xy[0], + debt=debt, + ) if final_debt == 0: - log UserState(user, 0, 0, 0, 0, 0) # Not logging partial removeal b/c we have not enough info + log IController.UserState( + user=user, collateral=0, debt=0, n1=0, n2=0, liquidation_discount=0 + ) # Not logging partial removeal b/c we have not enough info self._remove_from_list(user) - d: uint256 = self._total_debt.initial_debt * rate_mul / self._total_debt.rate_mul - self._total_debt.initial_debt = unsafe_sub(max(d, debt), debt) - self._total_debt.rate_mul = rate_mul - - self._save_rate() - - -@external -@nonreentrant('lock') -def liquidate(user: address, min_x: uint256): - """ - @notice Perform a bad liquidation (or self-liquidation) of user if health is not good - @param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched) - """ - discount: uint256 = 0 - if not self._check_approval(user): - discount = self.liquidation_discounts[user] - self._liquidate(user, min_x, discount, 10**18, empty(address), []) - - -@external -@nonreentrant('lock') -def liquidate_extended(user: address, min_x: uint256, frac: uint256, - callbacker: address, callback_args: DynArray[uint256,5], callback_bytes: Bytes[10**4] = b""): - """ - @notice Perform a bad liquidation (or self-liquidation) of user if health is not good - @param min_x Minimal amount of stablecoin to receive (to avoid liquidators being sandwiched) - @param frac Fraction to liquidate; 100% = 10**18 - @param callbacker Address of the callback contract - @param callback_args Extra arguments for the callback (up to 5) such as min_amount etc - """ - discount: uint256 = 0 - if not self._check_approval(user): - discount = self.liquidation_discounts[user] - self._liquidate(user, min_x, discount, min(frac, 10**18), callbacker, callback_args, callback_bytes) - @view @external -@nonreentrant('lock') -def tokens_to_liquidate(user: address, frac: uint256 = 10 ** 18) -> uint256: +def tokens_to_liquidate(user: address, frac: uint256 = WAD) -> uint256: """ - @notice Calculate the amount of stablecoins to have in liquidator's wallet to liquidate a user + @notice Calculate the amount of borrowed asset to have in liquidator's wallet to liquidate a user @param user Address of the user to liquidate @param frac Fraction to liquidate; 100% = 10**18 - @return The amount of stablecoins needed + @return The amount of borrowed asset needed """ health_limit: uint256 = 0 if not self._check_approval(user): health_limit = self.liquidation_discounts[user] - stablecoins: uint256 = unsafe_div(AMM.get_sum_xy(user)[0] * self._get_f_remove(frac, health_limit), 10 ** 18) - debt: uint256 = unsafe_div(self._debt(user)[0] * frac, 10 ** 18) + borrowed: uint256 = unsafe_div( + (staticcall AMM.get_sum_xy(user))[0] + * self._get_f_remove(frac, health_limit), + WAD, + ) + debt: uint256 = unsafe_div(self._debt(user)[0] * frac, WAD) - return unsafe_sub(max(debt, stablecoins), stablecoins) + return unsafe_sub(max(debt, borrowed), borrowed) @view @external -@nonreentrant('lock') def health(user: address, full: bool = False) -> int256: """ @notice Returns position health normalized to 1e18 for the user. Liquidation starts when < 0, however devaluation of collateral doesn't cause liquidation """ - return self._health(user, self._debt(user)[0], full, self.liquidation_discounts[user]) + return self._health( + user, self._debt(user)[0], full, self.liquidation_discounts[user] + ) @view @external -@nonreentrant('lock') -def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position, 1000]: +def users_to_liquidate( + _from: uint256 = 0, _limit: uint256 = 0 +) -> DynArray[IController.Position, 1000]: """ @notice Returns a dynamic array of users who can be "hard-liquidated". This method is designed for convenience of liquidation bots. @@ -1335,179 +1343,126 @@ def users_to_liquidate(_from: uint256=0, _limit: uint256=0) -> DynArray[Position @param _limit Number of loans to look over @return Dynamic array with detailed info about positions of users """ - n_loans: uint256 = self.n_loans - limit: uint256 = _limit - if _limit == 0: - limit = n_loans - ix: uint256 = _from - out: DynArray[Position, 1000] = [] - for i in range(10**6): - if ix >= n_loans or i == limit: - break - user: address = self.loans[ix] - debt: uint256 = self._debt(user)[0] - health: int256 = self._health(user, debt, True, self.liquidation_discounts[user]) - if health < 0: - xy: uint256[2] = AMM.get_sum_xy(user) - out.append(Position({ - user: user, - x: xy[0], - y: xy[1], - debt: debt, - health: health - })) - ix += 1 - return out - - -# AMM has a nonreentrant decorator + return staticcall self._view.users_to_liquidate(_from, _limit) + + @view @external +@reentrant def amm_price() -> uint256: """ @notice Current price from the AMM + @dev Marked as reentrant because AMM already has a nonreentrant decorator """ - return AMM.get_p() + return staticcall AMM.get_p() @view @external -@nonreentrant('lock') def user_prices(user: address) -> uint256[2]: # Upper, lower """ @notice Lowest price of the lower band and highest price of the upper band the user has deposit in the AMM @param user User address @return (upper_price, lower_price) """ - assert AMM.has_liquidity(user) - ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0] - return [AMM.p_oracle_up(ns[0]), AMM.p_oracle_down(ns[1])] + return staticcall self._view.user_prices(user) @view @external -@nonreentrant('lock') def user_state(user: address) -> uint256[4]: """ @notice Return the user state in one call @param user User to return the state for - @return (collateral, stablecoin, debt, N) + @return (collateral, borrowed, debt, N) """ - xy: uint256[2] = AMM.get_sum_xy(user) - ns: int256[2] = AMM.read_user_tick_numbers(user) # ns[1] > ns[0] - return [xy[1], xy[0], self._debt(user)[0], convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256)] + return staticcall self._view.user_state(user) -# AMM has nonreentrant decorator @external +@reentrant def set_amm_fee(fee: uint256): """ @notice Set the AMM fee (factory admin only) - @param fee The fee which should be no higher than MAX_FEE + @dev Reentrant because AMM is nonreentrant + @param fee The fee which should be no higher than MAX_AMM_FEE """ - assert msg.sender == FACTORY.admin() - assert fee <= MAX_FEE and fee >= MIN_FEE, "Fee" - AMM.set_fee(fee) + self._check_admin() + assert fee <= MAX_AMM_FEE and fee >= MIN_AMM_FEE, "Fee" + extcall AMM.set_fee(fee) -@nonreentrant('lock') @external -def set_monetary_policy(monetary_policy: address): +def set_monetary_policy(monetary_policy: IMonetaryPolicy): """ @notice Set monetary policy contract @param monetary_policy Address of the monetary policy contract """ - assert msg.sender == FACTORY.admin() - self.monetary_policy = MonetaryPolicy(monetary_policy) - MonetaryPolicy(monetary_policy).rate_write() - log SetMonetaryPolicy(monetary_policy) + self._check_admin() + self._monetary_policy = monetary_policy + extcall monetary_policy.rate_write() + log IController.SetMonetaryPolicy(monetary_policy=monetary_policy) -@nonreentrant('lock') @external -def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): +def set_borrowing_discounts( + loan_discount: uint256, liquidation_discount: uint256 +): """ @notice Set discounts at which we can borrow (defines max LTV) and where bad liquidation starts @param loan_discount Discount which defines LTV @param liquidation_discount Discount where bad liquidation starts """ - assert msg.sender == FACTORY.admin() + self._check_admin() assert loan_discount > liquidation_discount assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT assert loan_discount <= MAX_LOAN_DISCOUNT self.liquidation_discount = liquidation_discount self.loan_discount = loan_discount - log SetBorrowingDiscounts(loan_discount, liquidation_discount) + log IController.SetBorrowingDiscounts( + loan_discount=loan_discount, liquidation_discount=liquidation_discount + ) @external -@nonreentrant('lock') -def set_callback(cb: address): +def set_callback(cb: ILMGauge): """ @notice Set liquidity mining callback """ - assert msg.sender == FACTORY.admin() - AMM.set_callback(cb) - log SetLMCallback(cb) - - -@external -@view -def admin_fees() -> uint256: - """ - @notice Calculate the amount of fees obtained from the interest - """ - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul + self.redeemed - minted: uint256 = self.minted - return unsafe_sub(max(loan.initial_debt, minted), minted) + self._check_admin() + extcall AMM.set_callback(cb) + log IController.SetLMCallback(callback=cb) @external -@nonreentrant('lock') def collect_fees() -> uint256: """ @notice Collect the fees charged as interest. - None of this fees are collected if factory has no fee_receiver - e.g. for lending - This is by design: lending does NOT earn interest, system makes money by using crvUSD """ - # Calling fee_receiver will fail for lending markets because everything gets to lenders - _to: address = FACTORY.fee_receiver() + return self._collect_fees() - # Borrowing-based fees - rate_mul: uint256 = AMM.get_rate_mul() - loan: Loan = self._total_debt - loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul - loan.rate_mul = rate_mul - self._total_debt = loan + +@internal +def _collect_fees() -> uint256: + rate_mul: uint256 = staticcall AMM.get_rate_mul() + loan: IController.Loan = self._update_total_debt(0, rate_mul, False) + + pending_admin_fees: uint256 = self.admin_fees + self.collected += pending_admin_fees + self.admin_fees = 0 + tkn.transfer(BORROWED_TOKEN, staticcall FACTORY.fee_receiver(), pending_admin_fees) self._save_rate() + log IController.CollectFees(amount=pending_admin_fees, new_supply=loan.initial_debt) - # Amount which would have been redeemed if all the debt was repaid now - to_be_redeemed: uint256 = loan.initial_debt + self.redeemed - # Amount which was minted when borrowing + all previously claimed admin fees - minted: uint256 = self.minted - # Difference between to_be_redeemed and minted amount is exactly due to interest charged - if to_be_redeemed > minted: - self.minted = to_be_redeemed - to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge - self.transfer(BORROWED_TOKEN, _to, to_be_redeemed) - log CollectFees(to_be_redeemed, loan.initial_debt) - return to_be_redeemed - else: - log CollectFees(0, loan.initial_debt) - return 0 + return pending_admin_fees @external -@view -@nonreentrant('lock') -def check_lock() -> bool: - return True - +@reentrant # TODO check if needed +def _on_debt_increased(delta: uint256, total_debt: uint256): + pass -# Allowance methods @external def approve(_spender: address, _allow: bool): @@ -1517,7 +1472,7 @@ def approve(_spender: address, _allow: bool): @param _allow Whether to turn the approval on or off (no amounts) """ self.approval[msg.sender][_spender] = _allow - log Approval(msg.sender, _spender, _allow) + log IController.Approval(owner=msg.sender, spender=_spender, allow=_allow) @internal @@ -1533,4 +1488,4 @@ def set_extra_health(_value: uint256): @param _value 1e18-based addition to loan_discount """ self.extra_health[msg.sender] = _value - log SetExtraHealth(msg.sender, _value) + log IController.SetExtraHealth(user=msg.sender, health=_value) diff --git a/contracts/ControllerView.vy b/contracts/ControllerView.vy new file mode 100644 index 00000000..3b07324c --- /dev/null +++ b/contracts/ControllerView.vy @@ -0,0 +1,370 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +""" +@title Controller View Contract +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice This contract never requires any direct interaction as the + main controller contract forwards all relevant calls. +@custom:security security@curve.fi +""" + +from contracts.interfaces import IAMM +from contracts.interfaces import IController + +from curve_std.interfaces import IERC20 + +from contracts import Controller as core +import contracts.lib.liquidation_lib as liq +from contracts import constants as c +from snekmate.utils import math + +# https://github.com/vyperlang/vyper/issues/4723 +MIN_TICKS_UINT: constant(uint256) = c.MIN_TICKS_UINT +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES +WAD: constant(uint256) = c.WAD +SWAD: constant(int256) = c.SWAD +MAX_P_BASE_BANDS: constant(int256) = 5 +MAX_SKIP_TICKS: constant(uint256) = 1024 + +SQRT_BAND_RATIO: immutable(uint256) +LOGN_A_RATIO: immutable(int256) # log(A / (A - 1)) +A: immutable(uint256) +AMM: immutable(IAMM) +CONTROLLER: immutable(IController) +COLLATERAL_TOKEN: immutable(IERC20) +COLLATERAL_PRECISION: immutable(uint256) +BORROWED_TOKEN: immutable(IERC20) +BORROWED_PRECISION: immutable(uint256) + + +@deploy +def __init__( + _controller: IController, + _sqrt_band_ratio: uint256, + _logn_a_ratio: int256, + _amm: IAMM, + _A: uint256, + _collateral_token: IERC20, + _collateral_precision: uint256, + _borrowed_token: IERC20, + _borrowed_precision: uint256, +): + CONTROLLER = _controller + SQRT_BAND_RATIO = _sqrt_band_ratio + LOGN_A_RATIO = _logn_a_ratio + AMM = _amm + A = _A + COLLATERAL_TOKEN = _collateral_token + COLLATERAL_PRECISION = _collateral_precision + BORROWED_TOKEN = _borrowed_token + BORROWED_PRECISION = _borrowed_precision + + +@view +@internal +def _debt(_for: address) -> uint256: + return staticcall CONTROLLER.debt(_for) + + +@view +@internal +def _liquidation_discount() -> uint256: + return staticcall CONTROLLER.liquidation_discount() + + +@view +@internal +def _liquidation_discounts(_for: address) -> uint256: + return staticcall CONTROLLER.liquidation_discounts(_for) + + +@view +@internal +def _loan_discount() -> uint256: + return staticcall CONTROLLER.loan_discount() + + +@view +@internal +def _calculate_debt_n1( + collateral: uint256, + debt: uint256, + N: uint256, + user: address = empty(address), +) -> int256: + return staticcall CONTROLLER.calculate_debt_n1(collateral, debt, N, user) + + +@view +@internal +def _n_loans() -> uint256: + return staticcall CONTROLLER.n_loans() + + +@view +@internal +def _loans(_for: uint256) -> address: + return staticcall CONTROLLER.loans(_for) + + +@view +@internal +def _health(_for: address, full: bool = False) -> int256: + return staticcall CONTROLLER.health(_for, full) + + +@view +@internal +def _extra_health(_for: address) -> uint256: + return staticcall CONTROLLER.extra_health(_for) + + +@internal +@view +def _get_y_effective( + collateral: uint256, N: uint256, discount: uint256 +) -> uint256: + return core._get_y_effective(collateral, N, discount, SQRT_BAND_RATIO, A) + + +@external +@view +def health_calculator( + user: address, + d_collateral: int256, + d_debt: int256, + full: bool, + N: uint256 = 0, +) -> int256: + """ + @notice Natspec for this function is available in its controller contract + """ + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) + debt: int256 = convert(self._debt(user), int256) + n: uint256 = N + ld: int256 = 0 + if debt != 0: + ld = convert(self._liquidation_discounts(user), int256) + n = convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256) + else: + ld = convert(self._liquidation_discount(), int256) + ns[0] = max_value(int256) # This will trigger a "re-deposit" + + n1: int256 = 0 + collateral: int256 = 0 + x_eff: int256 = 0 + debt += d_debt + assert debt > 0, "debt<0" + + active_band: int256 = staticcall AMM.active_band_with_skip() + + if ns[0] > active_band: # re-deposit + collateral = ( + convert((staticcall AMM.get_sum_xy(user))[1], int256) + d_collateral + ) + n1 = self._calculate_debt_n1( + convert(collateral, uint256), convert(debt, uint256), n, user + ) + collateral *= convert( + COLLATERAL_PRECISION, int256 + ) # now has 18 decimals + else: + n1 = ns[0] + x_eff = convert( + staticcall AMM.get_x_down(user) + * unsafe_mul(WAD, BORROWED_PRECISION), + int256, + ) + + debt *= convert(BORROWED_PRECISION, int256) + + p0: int256 = convert(staticcall AMM.p_oracle_up(n1), int256) + if ns[0] > active_band: + x_eff = ( + convert( + self._get_y_effective(convert(collateral, uint256), n, 0), + int256, + ) + * p0 + ) + + health: int256 = unsafe_div(x_eff, debt) + health = health - unsafe_div(health * ld, SWAD) - SWAD + + if full: + if n1 > active_band: # We are not in liquidation mode + p_diff: int256 = ( + max(p0, convert(staticcall AMM.price_oracle(), int256)) - p0 + ) + if p_diff > 0: + health += unsafe_div(p_diff * collateral, debt) + return health + + +@view +@external +def users_to_liquidate( + _from: uint256 = 0, _limit: uint256 = 0 +) -> DynArray[IController.Position, 1000]: + """ + @notice Natspec for this function is available in its controller contract + """ + return liq.users_with_health( + CONTROLLER, + _from, + _limit, + 0, + False, + empty(address), + True, + ) + + +@view +@external +def user_prices(user: address) -> uint256[2]: # Upper, lower + """ + @notice Natspec for this function is available in its controller contract + """ + assert staticcall AMM.has_liquidity(user) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) # ns[1] > ns[0] + return [ + staticcall AMM.p_oracle_up(ns[0]), staticcall AMM.p_oracle_down(ns[1]) + ] + + +@view +@external +def user_state(user: address) -> uint256[4]: + """ + @notice Natspec for this function is available in its controller contract + """ + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + ns: int256[2] = staticcall AMM.read_user_tick_numbers(user) # ns[1] > ns[0] + return [ + xy[1], + xy[0], + self._debt(user), + convert(unsafe_add(unsafe_sub(ns[1], ns[0]), 1), uint256), + ] + + +@internal +@view +def _max_p_base() -> uint256: + """ + @notice Calculate max base price including skipping bands + """ + p_oracle: uint256 = staticcall AMM.price_oracle() + # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands + n1: int256 = math._wad_ln( + convert(staticcall AMM.get_base_price() * WAD // p_oracle, int256) + ) + if n1 < 0: + n1 -= ( + LOGN_A_RATIO - 1 + ) # This is to deal with vyper's rounding of negative numbers + n1 = unsafe_div(n1, LOGN_A_RATIO) + MAX_P_BASE_BANDS + n_min: int256 = staticcall AMM.active_band_with_skip() + n1 = max(n1, n_min + 1) + p_base: uint256 = staticcall AMM.p_oracle_up(n1) + + for i: uint256 in range(MAX_SKIP_TICKS + 1): + n1 -= 1 + if n1 <= n_min: + break + p_base_prev: uint256 = p_base + p_base = staticcall AMM.p_oracle_up(n1) + if p_base > p_oracle: + return p_base_prev + return p_base + + +@internal +@view +def _max_borrowable( + collateral: uint256, + N: uint256, + cap: uint256, + user: address, +) -> uint256: + + # Calculation of maximum which can be borrowed. + # It corresponds to a minimum between the amount corresponding to price_oracle + # and the one given by the min reachable band. + # + # Given by p_oracle (perhaps needs to be multiplied by (A - 1) / A to account for mid-band effects) + # x_max ~= y_effective * p_oracle + # + # Given by band number: + # if n1 is the lowest empty band in the AMM + # xmax ~= y_effective * amm.p_oracle_up(n1) + # + # When n1 -= 1: + # p_oracle_up *= A / (A - 1) + # if N < MIN_TICKS or N > MAX_TICKS: + assert N >= MIN_TICKS_UINT and N <= MAX_TICKS_UINT + + y_effective: uint256 = self._get_y_effective( + collateral * COLLATERAL_PRECISION, + N, + self._loan_discount() + self._extra_health(user), + ) + + x: uint256 = unsafe_sub( + max(unsafe_div(y_effective * self._max_p_base(), WAD), 1), 1 + ) + x = unsafe_div( + x * (WAD - 10**14), unsafe_mul(WAD, BORROWED_PRECISION) + ) # Make it a bit smaller + + return min(x, cap) + + +@external +@view +def max_borrowable( + collateral: uint256, + N: uint256, + current_debt: uint256 = 0, + user: address = empty(address), +) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + # Cannot borrow beyond the amount of coins Controller has + cap: uint256 = ( + staticcall BORROWED_TOKEN.balanceOf(CONTROLLER.address) + current_debt + ) + + return self._max_borrowable(collateral, N, cap, user) + + +@external +@view +def min_collateral( + debt: uint256, N: uint256, user: address = empty(address) +) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + # Add N**2 to account for precision loss in multiple bands, e.g. N / (y/N) = N**2 / y + assert N <= MAX_TICKS_UINT and N >= MIN_TICKS_UINT + return unsafe_div( + unsafe_div( + debt + * unsafe_mul(WAD, BORROWED_PRECISION) // self._max_p_base() + * WAD // self._get_y_effective( + WAD, N, self._loan_discount() + self._extra_health(user) + ) + + unsafe_add( + unsafe_mul(N, unsafe_add(N, 2 * DEAD_SHARES)), + unsafe_sub(COLLATERAL_PRECISION, 1), + ), + COLLATERAL_PRECISION, + ) + * WAD, + WAD - 10**14, + ) diff --git a/contracts/LMCallback.vy b/contracts/LMCallback.vy index ffb5f472..74f7da8d 100644 --- a/contracts/LMCallback.vy +++ b/contracts/LMCallback.vy @@ -1,4 +1,4 @@ -# @version 0.4.1 +# pragma version 0.4.3 """ @title LlamaLend LMCallback @author Curve.Fi @@ -6,14 +6,12 @@ @notice LM callback works like a gauge for collateral in LlamaLend/crvUSD AMMs """ -from ethereum.ercs import IERC20 +from curve_std.interfaces import IERC20 interface ILLAMMA: def coins(i: uint256) -> address: view def get_sum_xy(user: address) -> uint256[2]: view def read_user_tick_numbers(user: address) -> int256[2]: view - def read_user_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICKS_UINT]: view - def admin_fees_y() -> uint256: view def user_shares(user: address) -> UserTicks: view interface CRV20: @@ -157,10 +155,10 @@ def _checkpoint_collateral_shares(n_start: int256, collateral_per_share: DynArra new_rate = 0 # Transfers from/to AMM always happen after LM Callback calls, so this value is taken BEFORE the action - total_collateral: uint256 = staticcall COLLATERAL_TOKEN.balanceOf(AMM.address) - staticcall AMM.admin_fees_y() + total_collateral: uint256 = staticcall COLLATERAL_TOKEN.balanceOf(AMM.address) delta_rpc: uint256 = 0 - if total_collateral > 0 and block.timestamp > I_rpc.t: # XXX should we not loop when total_collateral == 0? + if total_collateral > 0 and block.timestamp > I_rpc.t: extcall GAUGE_CONTROLLER.checkpoint_gauge(self) prev_week_time: uint256 = I_rpc.t week_time: uint256 = min(unsafe_div(prev_week_time + WEEK, WEEK) * WEEK, block.timestamp) @@ -279,7 +277,7 @@ def total_collateral() -> uint256: """ @return Total collateral amount in LlamaLend/crvUSD AMM """ - return staticcall COLLATERAL_TOKEN.balanceOf(AMM.address) - staticcall AMM.admin_fees_y() + return staticcall COLLATERAL_TOKEN.balanceOf(AMM.address) @external @@ -362,7 +360,7 @@ def set_killed(_is_killed: bool): @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV @param _is_killed Killed status to set """ - assert msg.sender == staticcall LENDING_FACTORY.admin() # dev: only owner + assert msg.sender == staticcall LENDING_FACTORY.admin(), "only owner" self._checkpoint_collateral_shares(0, [], 0) self.is_killed = _is_killed log SetKilled(is_killed=_is_killed) diff --git a/contracts/MintController.vy b/contracts/MintController.vy new file mode 100644 index 00000000..d00e7afa --- /dev/null +++ b/contracts/MintController.vy @@ -0,0 +1,40 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize +""" +@title Mint Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice This is just a simple adapter not to have to deploy a new + factory for mint markets. +@custom:security security@curve.fi +""" + +from contracts import Controller as core + +initializes: core + +# Usually a bad practice to expose through +# `__interface__` but this contract is just +# an adapter to make the construct of Controller +# compatible with the old mint factory. +exports: core.__interface__ + + +@deploy +def __init__( + collateral_token: core.IERC20, + monetary_policy: core.IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + amm: core.IAMM, +): + core.__init__( + collateral_token, + staticcall core.IFactory(msg.sender).stablecoin(), + monetary_policy, + loan_discount, + liquidation_discount, + amm, + empty(address), # to replace at deployment with view blueprint + ) diff --git a/contracts/Stableswap.vy b/contracts/Stableswap.vy index 07bc2620..793ce7f9 100644 --- a/contracts/Stableswap.vy +++ b/contracts/Stableswap.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# @version 0.4.3 """ @title StableSwap @author Curve.Fi @@ -7,7 +7,7 @@ @dev ERC20 support for return True/revert, return True/False, return None """ -from vyper.interfaces import ERC20 +from curve_std.interfaces import IERC20 interface Factory: def get_fee_receiver(_pool: address) -> address: view @@ -129,7 +129,7 @@ ma_exp_time: public(uint256) ma_last_time: public(uint256) -@external +@deploy def __init__(): # we do this to prevent the implementation contract from being used as a pool self.factory = 0x0000000000000000000000000000000000000001 @@ -157,7 +157,7 @@ def initialize( # check if factory was already set to prevent initializing contract twice assert self.factory == empty(address) - for i in range(N_COINS): + for i: uint256 in range(N_COINS): coin: address = _coins[i] if coin == empty(address): break @@ -178,11 +178,11 @@ def initialize( self.symbol = concat(_symbol, "-f") self.DOMAIN_SEPARATOR = keccak256( - _abi_encode(EIP712_TYPEHASH, keccak256(name), keccak256(VERSION), chain.id, self) + abi_encode(EIP712_TYPEHASH, keccak256(name), keccak256(VERSION), chain.id, self) ) # fire a transfer event so block explorers identify the contract as an ERC20 - log Transfer(empty(address), self, 0) + log Transfer(sender=empty(address), receiver=self, value=0) ### ERC20 Functionality ### @@ -205,7 +205,7 @@ def _transfer(_from: address, _to: address, _value: uint256): self.balanceOf[_from] -= _value self.balanceOf[_to] += _value - log Transfer(_from, _to, _value) + log Transfer(sender=_from, receiver=_to, value=_value) @external @@ -250,7 +250,7 @@ def approve(_spender : address, _value : uint256) -> bool: """ self.allowance[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) + log Approval(owner=msg.sender, spender=_spender, value=_value) return True @@ -287,21 +287,21 @@ def permit( concat( b"\x19\x01", self.DOMAIN_SEPARATOR, - keccak256(_abi_encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonce, _deadline)) + keccak256(abi_encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonce, _deadline)) ) ) if _owner.is_contract: - sig: Bytes[65] = concat(_abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) + sig: Bytes[65] = concat(abi_encode(_r, _s), slice(convert(_v, bytes32), 31, 1)) # reentrancy not a concern since this is a staticcall - assert ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL + assert staticcall ERC1271(_owner).isValidSignature(digest, sig) == ERC1271_MAGIC_VAL else: assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner self.allowance[_owner][_spender] = _value self.nonces[_owner] = nonce + 1 - log Approval(_owner, _spender, _value) + log Approval(owner=_owner, spender=_spender, value=_value) return True @@ -312,7 +312,7 @@ def permit( def pack_prices(p1: uint256, p2: uint256) -> uint256: assert p1 < 2**128 assert p2 < 2**128 - return p1 | shift(p2, 128) + return p1 | (p2 << 128) @view @@ -324,7 +324,7 @@ def last_price() -> uint256: @view @external def ema_price() -> uint256: - return shift(self.last_prices_packed, -128) + return self.last_prices_packed >> 128 @view @@ -347,9 +347,9 @@ def _A() -> uint256: t0: uint256 = self.initial_A_time # Expressions in uint256 cannot have negative numbers, thus "if" if A1 > A0: - return A0 + (A1 - A0) * (block.timestamp - t0) / (t1 - t0) + return A0 + (A1 - A0) * (block.timestamp - t0) // (t1 - t0) else: - return A0 - (A0 - A1) * (block.timestamp - t0) / (t1 - t0) + return A0 - (A0 - A1) * (block.timestamp - t0) // (t1 - t0) else: # when t1 == 0 or block.timestamp >= t1 return A1 @@ -364,7 +364,7 @@ def admin_fee() -> uint256: @view @external def A() -> uint256: - return self._A() / A_PRECISION + return self._A() // A_PRECISION @view @@ -377,8 +377,8 @@ def A_precise() -> uint256: @internal def _xp_mem(_rates: uint256[N_COINS], _balances: uint256[N_COINS]) -> uint256[N_COINS]: result: uint256[N_COINS] = empty(uint256[N_COINS]) - for i in range(N_COINS): - result[i] = _rates[i] * _balances[i] / PRECISION + for i: uint256 in range(N_COINS): + result[i] = _rates[i] * _balances[i] // PRECISION return result @@ -389,23 +389,23 @@ def get_D(_xp: uint256[N_COINS], _amp: uint256) -> uint256: D invariant calculation in non-overflowing integer operations iteratively - A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) // (n**n * prod(x_i)) Converging solution: - D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) // (n**n prod(x_i))) // (A * n**n - 1) """ S: uint256 = 0 - for x in _xp: + for x: uint256 in _xp: S += x if S == 0: return 0 D: uint256 = S Ann: uint256 = _amp * N_COINS - for i in range(255): - D_P: uint256 = D * D / _xp[0] * D / _xp[1] / N_COINS**N_COINS + for i: uint256 in range(255): + D_P: uint256 = D * D // _xp[0] * D // _xp[1] // N_COINS**N_COINS Dprev: uint256 = D - D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) * D_P) + D = (Ann * S // A_PRECISION + D_P * N_COINS) * D // ((Ann - A_PRECISION) * D // A_PRECISION + (N_COINS + 1) * D_P) # Equality with the precision of 1 if D > Dprev: if D - Dprev <= 1: @@ -428,12 +428,12 @@ def get_D_mem(_rates: uint256[N_COINS], _balances: uint256[N_COINS], _amp: uint2 @internal @pure def _get_p(xp: uint256[N_COINS], amp: uint256, D: uint256) -> uint256: - # dx_0 / dx_1 only, however can have any number of coins in pool + # dx_0 // dx_1 only, however can have any number of coins in pool ANN: uint256 = amp * N_COINS - Dr: uint256 = D / (N_COINS**N_COINS) - for i in range(N_COINS): - Dr = Dr * D / xp[i] - return 10**18 * (ANN * xp[0] / A_PRECISION + Dr * xp[0] / xp[1]) / (ANN * xp[0] / A_PRECISION + Dr) + Dr: uint256 = D // (N_COINS**N_COINS) + for i: uint256 in range(N_COINS): + Dr = Dr * D // xp[i] + return 10**18 * (ANN * xp[0] // A_PRECISION + Dr * xp[0] // xp[1]) // (ANN * xp[0] // A_PRECISION + Dr) @external @@ -476,9 +476,14 @@ def exp(power: int256) -> uint256: q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573) q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023) - return shift( - unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667), - unsafe_sub(k, 195)) + scaled: uint256 = unsafe_mul( + convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667 + ) + shift_amount: int256 = unsafe_sub(k, 195) + if shift_amount >= 0: + return scaled << convert(shift_amount, uint256) + else: + return scaled >> convert(unsafe_sub(0, shift_amount), uint256) @internal @@ -488,11 +493,11 @@ def _ma_price() -> uint256: pp: uint256 = self.last_prices_packed last_price: uint256 = min(pp & (2**128 - 1), 2 * 10**18) - last_ema_price: uint256 = shift(pp, -128) + last_ema_price: uint256 = pp >> 128 if ma_last_time < block.timestamp: - alpha: uint256 = self.exp(- convert((block.timestamp - ma_last_time) * 10**18 / self.ma_exp_time, int256)) - return (last_price * (10**18 - alpha) + last_ema_price * alpha) / 10**18 + alpha: uint256 = self.exp(- convert((block.timestamp - ma_last_time) * 10**18 // self.ma_exp_time, int256)) + return (last_price * (10**18 - alpha) + last_ema_price * alpha) // 10**18 else: return last_ema_price @@ -536,7 +541,7 @@ def get_virtual_price() -> uint256: D: uint256 = self.get_D(xp, amp) # D is in the units similar to DAI (e.g. converted to precision 1e18) # When balanced, D = n * x_u - total virtual value of the portfolio - return D * PRECISION / self.totalSupply + return D * PRECISION // self.totalSupply @view @@ -557,7 +562,7 @@ def calc_token_amount(_amounts: uint256[N_COINS], _is_deposit: bool) -> uint256: total_supply: uint256 = self.totalSupply new_balances: uint256[N_COINS] = old_balances - for i in range(N_COINS): + for i: uint256 in range(N_COINS): amount: uint256 = _amounts[i] if _is_deposit: new_balances[i] += amount @@ -572,16 +577,16 @@ def calc_token_amount(_amounts: uint256[N_COINS], _is_deposit: bool) -> uint256: D2: uint256 = D1 if total_supply > 0: # Only account for fees if we are not the first to deposit - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) - for i in range(N_COINS): - ideal_balance: uint256 = D1 * old_balances[i] / D0 + base_fee: uint256 = self.fee * N_COINS // (4 * (N_COINS - 1)) + for i: uint256 in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] // D0 difference: uint256 = 0 new_balance: uint256 = new_balances[i] if ideal_balance > new_balance: difference = ideal_balance - new_balance else: difference = new_balance - ideal_balance - new_balances[i] -= base_fee * difference / FEE_DENOMINATOR + new_balances[i] -= base_fee * difference // FEE_DENOMINATOR xp: uint256[N_COINS] = self._xp_mem(rates, new_balances) D2 = self.get_D(xp, amp) else: @@ -593,11 +598,11 @@ def calc_token_amount(_amounts: uint256[N_COINS], _is_deposit: bool) -> uint256: diff = D2 - D0 else: diff = D0 - D2 - return diff * total_supply / D0 + return diff * total_supply // D0 @external -@nonreentrant('lock') +@nonreentrant def add_liquidity( _amounts: uint256[N_COINS], _min_mint_amount: uint256, @@ -619,10 +624,10 @@ def add_liquidity( total_supply: uint256 = self.totalSupply new_balances: uint256[N_COINS] = old_balances - for i in range(N_COINS): + for i: uint256 in range(N_COINS): amount: uint256 = _amounts[i] if amount > 0: - assert ERC20(self.coins[i]).transferFrom(msg.sender, self, amount, default_return_value=True) # dev: failed transfer + assert extcall IERC20(self.coins[i]).transferFrom(msg.sender, self, amount, default_return_value=True) # dev: failed transfer new_balances[i] += amount else: assert total_supply != 0 # dev: initial deposit requires all coins @@ -638,21 +643,21 @@ def add_liquidity( if total_supply > 0: # Only account for fees if we are not the first to deposit - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) - for i in range(N_COINS): - ideal_balance: uint256 = D1 * old_balances[i] / D0 + base_fee: uint256 = self.fee * N_COINS // (4 * (N_COINS - 1)) + for i: uint256 in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] // D0 difference: uint256 = 0 new_balance: uint256 = new_balances[i] if ideal_balance > new_balance: difference = ideal_balance - new_balance else: difference = new_balance - ideal_balance - fees[i] = base_fee * difference / FEE_DENOMINATOR - self.balances[i] = new_balance - (fees[i] * ADMIN_FEE / FEE_DENOMINATOR) + fees[i] = base_fee * difference // FEE_DENOMINATOR + self.balances[i] = new_balance - (fees[i] * ADMIN_FEE // FEE_DENOMINATOR) new_balances[i] -= fees[i] xp: uint256[N_COINS] = self._xp_mem(rates, new_balances) D2: uint256 = self.get_D(xp, amp) - mint_amount = total_supply * (D2 - D0) / D0 + mint_amount = total_supply * (D2 - D0) // D0 self.save_p(xp, amp, D2) else: @@ -665,9 +670,15 @@ def add_liquidity( total_supply += mint_amount self.balanceOf[_receiver] += mint_amount self.totalSupply = total_supply - log Transfer(empty(address), _receiver, mint_amount) - - log AddLiquidity(msg.sender, _amounts, fees, D1, total_supply) + log Transfer(sender=empty(address), receiver=_receiver, value=mint_amount) + + log AddLiquidity( + provider=msg.sender, + token_amounts=_amounts, + fees=fees, + invariant=D1, + token_supply=total_supply, + ) return mint_amount @@ -679,12 +690,12 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS], _amp: uint256, Calculate x[j] if one makes x[i] = x Done by solving quadratic equation iteratively. - x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D // (A * n**n)) = D ** (n + 1) // (n ** (2 * n) * prod' * A) x_1**2 + b*x_1 = c - x_1 = (x_1**2 + c) / (2*x_1 + b) + x_1 = (x_1**2 + c) // (2*x_1 + b) """ - # x in the input is converted to the same price/precision + # x in the input is converted to the same price//precision assert i != j # dev: same coin assert j >= 0 # dev: j below zero @@ -705,7 +716,7 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS], _amp: uint256, c: uint256 = D Ann: uint256 = amp * N_COINS - for _i in range(N_COINS_128): + for _i: int128 in range(N_COINS_128): if _i == i: _x = x elif _i != j: @@ -713,15 +724,15 @@ def get_y(i: int128, j: int128, x: uint256, xp: uint256[N_COINS], _amp: uint256, else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D // (_x * N_COINS) - c = c * D * A_PRECISION / (Ann * N_COINS) - b: uint256 = S_ + D * A_PRECISION / Ann # - D + c = c * D * A_PRECISION // (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION // Ann # - D y: uint256 = D - for _i in range(255): + for _i: uint256 in range(255): y_prev = y - y = (y*y + c) / (2 * y + b - D) + y = (y*y + c) // (2 * y + b - D) # Equality with the precision of 1 if y > y_prev: if y - y_prev <= 1: @@ -746,11 +757,11 @@ def get_dy(i: int128, j: int128, dx: uint256) -> uint256: rates: uint256[N_COINS] = self.rate_multipliers xp: uint256[N_COINS] = self._xp_mem(rates, self.balances) - x: uint256 = xp[i] + (dx * rates[i] / PRECISION) + x: uint256 = xp[i] + (dx * rates[i] // PRECISION) y: uint256 = self.get_y(i, j, x, xp, 0, 0) dy: uint256 = xp[j] - y - 1 - fee: uint256 = self.fee * dy / FEE_DENOMINATOR - return (dy - fee) * PRECISION / rates[j] + fee: uint256 = self.fee * dy // FEE_DENOMINATOR + return (dy - fee) * PRECISION // rates[j] @view @@ -767,13 +778,13 @@ def get_dx(i: int128, j: int128, dy: uint256) -> uint256: rates: uint256[N_COINS] = self.rate_multipliers xp: uint256[N_COINS] = self._xp_mem(rates, self.balances) - y: uint256 = xp[j] - (dy * rates[j] / PRECISION + 1) * FEE_DENOMINATOR / (FEE_DENOMINATOR - self.fee) + y: uint256 = xp[j] - (dy * rates[j] // PRECISION + 1) * FEE_DENOMINATOR // (FEE_DENOMINATOR - self.fee) x: uint256 = self.get_y(j, i, y, xp, 0, 0) - return (x - xp[i]) * PRECISION / rates[i] + return (x - xp[i]) * PRECISION // rates[i] @external -@nonreentrant('lock') +@nonreentrant def exchange( i: int128, j: int128, @@ -794,17 +805,17 @@ def exchange( old_balances: uint256[N_COINS] = self.balances xp: uint256[N_COINS] = self._xp_mem(rates, old_balances) - x: uint256 = xp[i] + _dx * rates[i] / PRECISION + x: uint256 = xp[i] + _dx * rates[i] // PRECISION amp: uint256 = self._A() D: uint256 = self.get_D(xp, amp) y: uint256 = self.get_y(i, j, x, xp, amp, D) dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors - dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR + dy_fee: uint256 = dy * self.fee // FEE_DENOMINATOR # Convert all to real units - dy = (dy - dy_fee) * PRECISION / rates[j] + dy = (dy - dy_fee) * PRECISION // rates[j] assert dy >= _min_dy, "Exchange resulted in fewer coins than expected" # xp is not used anymore, so we reuse it for price calc @@ -813,24 +824,30 @@ def exchange( # D is not changed because we did not apply a fee self.save_p(xp, amp, D) - dy_admin_fee: uint256 = dy_fee * ADMIN_FEE / FEE_DENOMINATOR - dy_admin_fee = dy_admin_fee * PRECISION / rates[j] + dy_admin_fee: uint256 = dy_fee * ADMIN_FEE // FEE_DENOMINATOR + dy_admin_fee = dy_admin_fee * PRECISION // rates[j] # Change balances exactly in same way as we change actual ERC20 coin amounts self.balances[i] = old_balances[i] + _dx # When rounding errors happen, we undercharge admin fee in favor of LP self.balances[j] = old_balances[j] - dy - dy_admin_fee - assert ERC20(self.coins[i]).transferFrom(msg.sender, self, _dx, default_return_value=True) # dev: failed transfer - assert ERC20(self.coins[j]).transfer(_receiver, dy, default_return_value=True) # dev: failed transfer + assert extcall IERC20(self.coins[i]).transferFrom(msg.sender, self, _dx, default_return_value=True) # dev: failed transfer + assert extcall IERC20(self.coins[j]).transfer(_receiver, dy, default_return_value=True) # dev: failed transfer - log TokenExchange(msg.sender, i, _dx, j, dy) + log TokenExchange( + buyer=msg.sender, + sold_id=i, + tokens_sold=_dx, + bought_id=j, + tokens_bought=dy, + ) return dy @external -@nonreentrant('lock') +@nonreentrant def remove_liquidity( _burn_amount: uint256, _min_amounts: uint256[N_COINS], @@ -847,26 +864,31 @@ def remove_liquidity( total_supply: uint256 = self.totalSupply amounts: uint256[N_COINS] = empty(uint256[N_COINS]) - for i in range(N_COINS): + for i: uint256 in range(N_COINS): old_balance: uint256 = self.balances[i] - value: uint256 = old_balance * _burn_amount / total_supply + value: uint256 = old_balance * _burn_amount // total_supply assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected" self.balances[i] = old_balance - value amounts[i] = value - assert ERC20(self.coins[i]).transfer(_receiver, value, default_return_value=True) # dev: failed transfer + assert extcall IERC20(self.coins[i]).transfer(_receiver, value, default_return_value=True) # dev: failed transfer total_supply -= _burn_amount self.balanceOf[msg.sender] -= _burn_amount self.totalSupply = total_supply - log Transfer(msg.sender, empty(address), _burn_amount) + log Transfer(sender=msg.sender, receiver=empty(address), value=_burn_amount) - log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply) + log RemoveLiquidity( + provider=msg.sender, + token_amounts=amounts, + fees=empty(uint256[N_COINS]), + token_supply=total_supply, + ) return amounts @external -@nonreentrant('lock') +@nonreentrant def remove_liquidity_imbalance( _amounts: uint256[N_COINS], _max_burn_amount: uint256, @@ -885,26 +907,26 @@ def remove_liquidity_imbalance( D0: uint256 = self.get_D_mem(rates, old_balances, amp) new_balances: uint256[N_COINS] = old_balances - for i in range(N_COINS): + for i: uint256 in range(N_COINS): amount: uint256 = _amounts[i] if amount != 0: new_balances[i] -= amount - assert ERC20(self.coins[i]).transfer(_receiver, amount, default_return_value=True) # dev: failed transfer + assert extcall IERC20(self.coins[i]).transfer(_receiver, amount, default_return_value=True) # dev: failed transfer D1: uint256 = self.get_D_mem(rates, new_balances, amp) fees: uint256[N_COINS] = empty(uint256[N_COINS]) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) - for i in range(N_COINS): - ideal_balance: uint256 = D1 * old_balances[i] / D0 + base_fee: uint256 = self.fee * N_COINS // (4 * (N_COINS - 1)) + for i: uint256 in range(N_COINS): + ideal_balance: uint256 = D1 * old_balances[i] // D0 difference: uint256 = 0 new_balance: uint256 = new_balances[i] if ideal_balance > new_balance: difference = ideal_balance - new_balance else: difference = new_balance - ideal_balance - fees[i] = base_fee * difference / FEE_DENOMINATOR - self.balances[i] = new_balance - (fees[i] * ADMIN_FEE / FEE_DENOMINATOR) + fees[i] = base_fee * difference // FEE_DENOMINATOR + self.balances[i] = new_balance - (fees[i] * ADMIN_FEE // FEE_DENOMINATOR) new_balances[i] -= fees[i] new_balances = self._xp_mem(rates, new_balances) D2: uint256 = self.get_D(new_balances, amp) @@ -912,15 +934,21 @@ def remove_liquidity_imbalance( self.save_p(new_balances, amp, D2) total_supply: uint256 = self.totalSupply - burn_amount: uint256 = ((D0 - D2) * total_supply / D0) + 1 - assert burn_amount > 1 # dev: zero tokens burned + burn_amount: uint256 = ((D0 - D2) * total_supply // D0) + 1 + assert burn_amount > 1, "zero tokens burned" assert burn_amount <= _max_burn_amount, "Slippage screwed you" total_supply -= burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= burn_amount - log Transfer(msg.sender, empty(address), burn_amount) - log RemoveLiquidityImbalance(msg.sender, _amounts, fees, D1, total_supply) + log Transfer(sender=msg.sender, receiver=empty(address), value=burn_amount) + log RemoveLiquidityImbalance( + provider=msg.sender, + token_amounts=_amounts, + fees=fees, + invariant=D1, + token_supply=total_supply, + ) return burn_amount @@ -932,12 +960,12 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: Calculate x[i] if one reduces D from being calculated for xp to D Done by solving quadratic equation iteratively. - x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + x_1 * (sum' - (A*n**n - 1) * D // (A * n**n)) = D ** (n + 1) // (n ** (2 * n) * prod' * A) x_1**2 + b*x_1 = c - x_1 = (x_1**2 + c) / (2*x_1 + b) + x_1 = (x_1**2 + c) // (2*x_1 + b) """ - # x in the input is converted to the same price/precision + # x in the input is converted to the same price//precision assert i >= 0 # dev: i below zero assert i < N_COINS_128 # dev: i above N_COINS @@ -948,21 +976,21 @@ def get_y_D(A: uint256, i: int128, xp: uint256[N_COINS], D: uint256) -> uint256: c: uint256 = D Ann: uint256 = A * N_COINS - for _i in range(N_COINS_128): + for _i: int128 in range(N_COINS_128): if _i != i: _x = xp[_i] else: continue S_ += _x - c = c * D / (_x * N_COINS) + c = c * D // (_x * N_COINS) - c = c * D * A_PRECISION / (Ann * N_COINS) - b: uint256 = S_ + D * A_PRECISION / Ann + c = c * D * A_PRECISION // (Ann * N_COINS) + b: uint256 = S_ + D * A_PRECISION // Ann y: uint256 = D - for _i in range(255): + for _i: uint256 in range(255): y_prev = y - y = (y*y + c) / (2 * y + b - D) + y = (y*y + c) // (2 * y + b - D) # Equality with the precision of 1 if y > y_prev: if y - y_prev <= 1: @@ -985,24 +1013,24 @@ def _calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256[3]: D0: uint256 = self.get_D(xp, amp) total_supply: uint256 = self.totalSupply - D1: uint256 = D0 - _burn_amount * D0 / total_supply + D1: uint256 = D0 - _burn_amount * D0 // total_supply new_y: uint256 = self.get_y_D(amp, i, xp, D1) - base_fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) + base_fee: uint256 = self.fee * N_COINS // (4 * (N_COINS - 1)) xp_reduced: uint256[N_COINS] = empty(uint256[N_COINS]) - for j in range(N_COINS_128): + for j: int128 in range(N_COINS_128): dx_expected: uint256 = 0 xp_j: uint256 = xp[j] if j == i: - dx_expected = xp_j * D1 / D0 - new_y + dx_expected = xp_j * D1 // D0 - new_y else: - dx_expected = xp_j - xp_j * D1 / D0 - xp_reduced[j] = xp_j - base_fee * dx_expected / FEE_DENOMINATOR + dx_expected = xp_j - xp_j * D1 // D0 + xp_reduced[j] = xp_j - base_fee * dx_expected // FEE_DENOMINATOR dy: uint256 = xp_reduced[i] - self.get_y_D(amp, i, xp_reduced, D1) - dy_0: uint256 = (xp[i] - new_y) * PRECISION / rates[i] # w/o fees - dy = (dy - 1) * PRECISION / rates[i] # Withdraw less to account for rounding errors + dy_0: uint256 = (xp[i] - new_y) * PRECISION // rates[i] # w/o fees + dy = (dy - 1) * PRECISION // rates[i] # Withdraw less to account for rounding errors xp[i] = new_y last_p: uint256 = 0 @@ -1025,7 +1053,7 @@ def calc_withdraw_one_coin(_burn_amount: uint256, i: int128) -> uint256: @external -@nonreentrant('lock') +@nonreentrant def remove_liquidity_one_coin( _burn_amount: uint256, i: int128, @@ -1043,14 +1071,19 @@ def remove_liquidity_one_coin( dy: uint256[3] = self._calc_withdraw_one_coin(_burn_amount, i) assert dy[0] >= _min_received, "Not enough coins removed" - self.balances[i] -= (dy[0] + dy[1] * ADMIN_FEE / FEE_DENOMINATOR) + self.balances[i] -= (dy[0] + dy[1] * ADMIN_FEE // FEE_DENOMINATOR) total_supply: uint256 = self.totalSupply - _burn_amount self.totalSupply = total_supply self.balanceOf[msg.sender] -= _burn_amount - log Transfer(msg.sender, empty(address), _burn_amount) - - assert ERC20(self.coins[i]).transfer(_receiver, dy[0], default_return_value=True) # dev: failed transfer - log RemoveLiquidityOne(msg.sender, _burn_amount, dy[0], total_supply) + log Transfer(sender=msg.sender, receiver=empty(address), value=_burn_amount) + + assert extcall IERC20(self.coins[i]).transfer(_receiver, dy[0], default_return_value=True) # dev: failed transfer + log RemoveLiquidityOne( + provider=msg.sender, + token_amount=_burn_amount, + coin_amount=dy[0], + token_supply=total_supply, + ) self.save_p_from_price(dy[2]) @@ -1059,7 +1092,7 @@ def remove_liquidity_one_coin( @external def ramp_A(_future_A: uint256, _future_time: uint256): - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == staticcall Factory(self.factory).admin() # dev: only owner assert block.timestamp >= self.initial_A_time + MIN_RAMP_TIME assert _future_time >= block.timestamp + MIN_RAMP_TIME # dev: insufficient time @@ -1077,12 +1110,17 @@ def ramp_A(_future_A: uint256, _future_time: uint256): self.initial_A_time = block.timestamp self.future_A_time = _future_time - log RampA(_initial_A, _future_A_p, block.timestamp, _future_time) + log RampA( + old_A=_initial_A, + new_A=_future_A_p, + initial_time=block.timestamp, + future_time=_future_time, + ) @external def stop_ramp_A(): - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == staticcall Factory(self.factory).admin() # dev: only owner current_A: uint256 = self._A() self.initial_A = current_A @@ -1091,12 +1129,12 @@ def stop_ramp_A(): self.future_A_time = block.timestamp # now (block.timestamp < t1) is always False, so we return saved A - log StopRampA(current_A, block.timestamp) + log StopRampA(A=current_A, t=block.timestamp) @external def set_ma_exp_time(_ma_exp_time: uint256): - assert msg.sender == Factory(self.factory).admin() # dev: only owner + assert msg.sender == staticcall Factory(self.factory).admin() # dev: only owner assert _ma_exp_time != 0 self.ma_exp_time = _ma_exp_time @@ -1105,40 +1143,40 @@ def set_ma_exp_time(_ma_exp_time: uint256): @view @external def admin_balances(i: uint256) -> uint256: - return ERC20(self.coins[i]).balanceOf(self) - self.balances[i] + return staticcall IERC20(self.coins[i]).balanceOf(self) - self.balances[i] @external def commit_new_fee(_new_fee: uint256): - assert msg.sender == Factory(self.factory).admin() + assert msg.sender == staticcall Factory(self.factory).admin() assert _new_fee <= MAX_FEE assert self.admin_action_deadline == 0 self.future_fee = _new_fee self.admin_action_deadline = block.timestamp + ADMIN_ACTIONS_DEADLINE_DT - log CommitNewFee(_new_fee) + log CommitNewFee(new_fee=_new_fee) @external def apply_new_fee(): - assert msg.sender == Factory(self.factory).admin() + assert msg.sender == staticcall Factory(self.factory).admin() deadline: uint256 = self.admin_action_deadline assert deadline != 0 and block.timestamp >= deadline fee: uint256 = self.future_fee self.fee = fee self.admin_action_deadline = 0 - log ApplyNewFee(fee) + log ApplyNewFee(fee=fee) @external def withdraw_admin_fees(): - receiver: address = Factory(self.factory).get_fee_receiver(self) + receiver: address = staticcall Factory(self.factory).get_fee_receiver(self) - for i in range(N_COINS): + for i: uint256 in range(N_COINS): coin: address = self.coins[i] - fees: uint256 = ERC20(coin).balanceOf(self) - self.balances[i] - assert ERC20(coin).transfer(receiver, fees, default_return_value=True) + fees: uint256 = staticcall IERC20(coin).balanceOf(self) - self.balances[i] + assert extcall IERC20(coin).transfer(receiver, fees, default_return_value=True) @pure diff --git a/contracts/constants.vy b/contracts/constants.vy new file mode 100644 index 00000000..e4b517b7 --- /dev/null +++ b/contracts/constants.vy @@ -0,0 +1,10 @@ +__version__: constant(String[5]) = "2.0.0" +MAX_TICKS_UINT: constant(uint256) = 50 +MIN_TICKS_UINT: constant(uint256) = 4 +MAX_TICKS: constant(int256) = 50 + +DEAD_SHARES: constant(uint256) = 1000 +WAD: constant(uint256) = 10**18 +SWAD: constant(int256) = 10**18 + +CALLDATA_MAX_SIZE: constant(uint256) = 10**4 diff --git a/contracts/interfaces/IAMM.vyi b/contracts/interfaces/IAMM.vyi new file mode 100644 index 00000000..70e0fccb --- /dev/null +++ b/contracts/interfaces/IAMM.vyi @@ -0,0 +1,310 @@ +from contracts.interfaces import ILMGauge +from contracts.interfaces import IPriceOracle +# Structs + +struct UserTicks: + ns: int256 + ticks: uint256[25] + + +struct DetailedTrade: + in_amount: uint256 + out_amount: uint256 + n1: int256 + n2: int256 + ticks_in: DynArray[uint256, 50] + last_tick_j: uint256 + + +# Events + +event TokenExchange: + buyer: address + sold_id: uint256 + tokens_sold: uint256 + bought_id: uint256 + tokens_bought: uint256 + + +event Deposit: + provider: address + amount: uint256 + n1: int256 + n2: int256 + + +event Withdraw: + provider: address + amount_borrowed: uint256 + amount_collateral: uint256 + + +event SetRate: + rate: uint256 + rate_mul: uint256 + time: uint256 + + +event SetFee: + fee: uint256 + + +event SetPriceOracle: + price_oracle: IPriceOracle + + +# Functions + +@external +def set_admin(_admin: address): + ... + + +@view +@external +def coins(i: uint256) -> address: + ... + + +@view +@external +def price_oracle() -> uint256: + ... + + +@view +@external +def dynamic_fee() -> uint256: + ... + + +@view +@external +def get_rate_mul() -> uint256: + ... + + +@view +@external +def get_base_price() -> uint256: + ... + + +@view +@external +def p_current_up(n: int256) -> uint256: + ... + + +@view +@external +def p_current_down(n: int256) -> uint256: + ... + + +@view +@external +def p_oracle_up(n: int256) -> uint256: + ... + + +@view +@external +def p_oracle_down(n: int256) -> uint256: + ... + + +@view +@external +def get_p() -> uint256: + ... + + +@view +@external +def read_user_tick_numbers(user: address) -> int256[2]: + ... + + +@view +@external +def can_skip_bands(n_end: int256) -> bool: + ... + + +@view +@external +def active_band_with_skip() -> int256: + ... + + +@view +@external +def has_liquidity(user: address) -> bool: + ... + + +@external +def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): + ... + + +@external +def withdraw(user: address, frac: uint256) -> uint256[2]: + ... + + +@view +@external +def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: + ... + + +@view +@external +def get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256): + ... + + +@view +@external +def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: + ... + + +@view +@external +def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): + ... + + +@external +def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address) -> uint256[2]: + ... + + +@external +def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address) -> uint256[2]: + ... + + +@view +@external +def get_y_up(user: address) -> uint256: + ... + + +@view +@external +def get_x_down(user: address) -> uint256: + ... + + +@view +@external +def get_sum_xy(user: address) -> uint256[2]: + ... + + +@view +@external +def get_xy(user: address) -> DynArray[uint256, 50][2]: + ... + + +@view +@external +def get_amount_for_price(p: uint256) -> (uint256, bool): + ... + + +@external +def set_rate(rate: uint256) -> uint256: + ... + + +@external +def set_fee(fee: uint256): + ... + + +@external +def set_callback(liquidity_mining_callback: ILMGauge): + ... + + +@external +def set_price_oracle(_price_oracle: IPriceOracle): + ... + + +@view +@external +def admin() -> address: + ... + + +@view +@external +def A() -> uint256: + ... + + +@view +@external +def fee() -> uint256: + ... + + +@view +@external +def rate() -> uint256: + ... + + +@view +@external +def active_band() -> int256: + ... + + +@view +@external +def min_band() -> int256: + ... + + +@view +@external +def max_band() -> int256: + ... + + +@view +@external +def price_oracle_contract() -> IPriceOracle: + ... + + +@view +@external +def bands_x(arg0: int256) -> uint256: + ... + + +@view +@external +def bands_y(arg0: int256) -> uint256: + ... + + +@view +@external +def user_shares(arg0: address) -> UserTicks: + ... + + +@view +@external +def liquidity_mining_callback() -> ILMGauge: + ... diff --git a/contracts/interfaces/IController.vyi b/contracts/interfaces/IController.vyi new file mode 100644 index 00000000..105cfab5 --- /dev/null +++ b/contracts/interfaces/IController.vyi @@ -0,0 +1,351 @@ +from curve_std.interfaces import IERC20 +from contracts.interfaces import ILMGauge +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IFactory + +# Events + +event Repay: + user: address + collateral_decrease: uint256 + loan_decrease: uint256 + + +event Liquidate: + liquidator: address + user: address + collateral_received: uint256 + borrowed_received: uint256 + debt: uint256 + + +event UserState: + user: address + collateral: uint256 + debt: uint256 + n1: int256 + n2: int256 + liquidation_discount: uint256 + + +event RemoveCollateral: + user: address + collateral_decrease: uint256 + + +event Borrow: + user: address + collateral_increase: uint256 + loan_increase: uint256 + + +event SetLMCallback: + callback: ILMGauge + + +event SetBorrowingDiscounts: + loan_discount: uint256 + liquidation_discount: uint256 + + +event SetMonetaryPolicy: + monetary_policy: IMonetaryPolicy + + +event Approval: + owner: address + spender: address + allow: bool + + +event SetExtraHealth: + user: address + health: uint256 + + +event CollectFees: + amount: uint256 + new_supply: uint256 + + +event SetView: + view: address + + +# Structs + +struct Position: + user: address + x: uint256 + y: uint256 + debt: uint256 + health: int256 + + +struct Loan: + initial_debt: uint256 + rate_mul: uint256 + + +struct CallbackData: + active_band: int256 + borrowed: uint256 + collateral: uint256 + +# Functions + +@view +@external +def monetary_policy() -> IMonetaryPolicy: + ... + + +@view +@external +def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: address) -> uint256: + ... + + +@external +def liquidate(user: address, min_x: uint256, _frac: uint256, callbacker: address = empty(address), calldata: Bytes[10000] = b''): + ... + + +@external +def borrow_more(collateral: uint256, debt: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def set_callback(cb: ILMGauge): + ... + + +@external +def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): + ... + + +@external +def set_monetary_policy(monetary_policy: IMonetaryPolicy): + ... + + +@external +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def set_amm_fee(fee: uint256): + ... + + +@external +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10000] = b"", + shrink: bool = False, +): + ... + + +@view +@external +def tokens_to_shrink(user: address) -> uint256: + ... + + +@external +def approve(_spender: address, _allow: bool): + ... + + +@external +def set_extra_health(_value: uint256): + ... + + +@external +def save_rate(): + ... + + +@external +def collect_fees() -> uint256: + ... + + +@external +def add_collateral(collateral: uint256, _for: address): + ... + + +@external +def remove_collateral(collateral: uint256, _for: address): + ... + + +@view +@external +def amm() -> IAMM: + ... + + +@view +@external +def collateral_token() -> IERC20: + ... + + +@view +@external +def borrowed_token() -> IERC20: + ... + + +@view +@external +def debt(user: address) -> uint256: + ... + + +@view +@external +def loan_exists(user: address) -> bool: + ... + + +@view +@external +def total_debt() -> uint256: + ... + + +@view +@external +def min_collateral(debt: uint256, N: uint256, user: address) -> uint256: + ... + + +@view +@external +def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256: + ... + + +@view +@external +def user_prices(user: address) -> uint256[2]: + ... + + +@view +@external +def amm_price() -> uint256: + ... + + +@view +@external +def user_state(user: address) -> uint256[4]: + ... + + +@view +@external +def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256) -> int256: + ... + + +@view +@external +def tokens_to_liquidate(user: address, frac: uint256) -> uint256: + ... + + +@view +@external +def health(user: address, full: bool) -> int256: + ... + + +@view +@external +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[Position, 1000]: + ... + + +@view +@external +def admin_fees() -> uint256: + ... + + +@view +@external +def factory() -> IFactory: + ... + + +@view +@external +def liquidation_discount() -> uint256: + ... + + +@view +@external +def loan_discount() -> uint256: + ... + + +@view +@external +def approval(arg0: address, arg1: address) -> bool: + ... + + +@view +@external +def extra_health(arg0: address) -> uint256: + ... + + +@view +@external +def liquidation_discounts(arg0: address) -> uint256: + ... + + +@view +@external +def loans(arg0: uint256) -> address: + ... + + +@view +@external +def loan_ix(arg0: address) -> uint256: + ... + + +@view +@external +def n_loans() -> uint256: + ... + + +@view +@external +def version() -> String[1]: + ... + + +@external +def set_view(view_impl: address): + ... diff --git a/contracts/interfaces/IControllerView.vyi b/contracts/interfaces/IControllerView.vyi new file mode 100644 index 00000000..b64b949a --- /dev/null +++ b/contracts/interfaces/IControllerView.vyi @@ -0,0 +1,38 @@ +from contracts.interfaces import IController + +# Functions + +@view +@external +def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256) -> int256: + ... + + +@view +@external +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IController.Position, 1000]: + ... + + +@view +@external +def user_prices(user: address) -> uint256[2]: + ... + + +@view +@external +def user_state(user: address) -> uint256[4]: + ... + + +@view +@external +def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: address) -> uint256: + ... + + +@view +@external +def min_collateral(debt: uint256, N: uint256, user: address) -> uint256: + ... diff --git a/contracts/interfaces/ICryptoPool.vyi b/contracts/interfaces/ICryptoPool.vyi new file mode 100644 index 00000000..cc924a52 --- /dev/null +++ b/contracts/interfaces/ICryptoPool.vyi @@ -0,0 +1,3 @@ +@view +def lp_price() -> uint256: + ... diff --git a/contracts/interfaces/IFactory.vyi b/contracts/interfaces/IFactory.vyi new file mode 100644 index 00000000..4c16bfc3 --- /dev/null +++ b/contracts/interfaces/IFactory.vyi @@ -0,0 +1,15 @@ +from curve_std.interfaces import IERC20 + +@view +def stablecoin() -> IERC20: + ... + +@view +def admin() -> address: + ... + +@view +def fee_receiver() -> address: + ... + +# TODO double check view decorators on non generated interfaces diff --git a/contracts/interfaces/ILMGauge.vyi b/contracts/interfaces/ILMGauge.vyi new file mode 100644 index 00000000..7c7b8042 --- /dev/null +++ b/contracts/interfaces/ILMGauge.vyi @@ -0,0 +1,8 @@ +from contracts import constants as c +MAX_TICKS_UINT: constant(uint256) = c.MAX_TICKS_UINT + +def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT], size: uint256): + ... + +def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT], size: uint256): + ... diff --git a/contracts/interfaces/ILendingFactory.vyi b/contracts/interfaces/ILendingFactory.vyi new file mode 100644 index 00000000..a288ed9c --- /dev/null +++ b/contracts/interfaces/ILendingFactory.vyi @@ -0,0 +1,44 @@ +from curve_std.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IVault +from contracts.interfaces import IPriceOracle +from contracts.interfaces import IMonetaryPolicy + + +event SetBlueprints: + amm: address + controller: address + vault: address + controller_view: address + + +event SetFeeReceiver: + fee_receiver: address + + +event NewVault: + id: indexed(uint256) + collateral_token: indexed(IERC20) + borrowed_token: indexed(IERC20) + vault: IVault + controller: IController + amm: IAMM + price_oracle: IPriceOracle + monetary_policy: IMonetaryPolicy + + +def create( + _borrowed_token: IERC20, + _collateral_token: IERC20, + _A: uint256, + _fee: uint256, + _loan_discount: uint256, + _liquidation_discount: uint256, + _price_oracle: IPriceOracle, + _monetary_policy: IMonetaryPolicy, + _name: String[64], + _supply_limit: uint256, +) -> address[3]: + ... +# TODO populate this diff --git a/contracts/interfaces/ILlamalendController.vyi b/contracts/interfaces/ILlamalendController.vyi new file mode 100644 index 00000000..088788cf --- /dev/null +++ b/contracts/interfaces/ILlamalendController.vyi @@ -0,0 +1,363 @@ +from curve_std.interfaces import IERC20 +from contracts.interfaces import ILMGauge +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IFactory +from contracts.interfaces import IController + +# TODO rename to ILendController +# TODO only keep diff + +# Events + +event Repay: + user: address + collateral_decrease: uint256 + loan_decrease: uint256 + + +event Liquidate: + liquidator: address + user: address + collateral_received: uint256 + borrowed_received: uint256 + debt: uint256 + + +event UserState: + user: address + collateral: uint256 + debt: uint256 + n1: int256 + n2: int256 + liquidation_discount: uint256 + + +event RemoveCollateral: + user: address + collateral_decrease: uint256 + + +event Borrow: + user: address + collateral_increase: uint256 + loan_increase: uint256 + + +event SetLMCallback: + callback: ILMGauge + + +event SetBorrowingDiscounts: + loan_discount: uint256 + liquidation_discount: uint256 + + +event SetMonetaryPolicy: + monetary_policy: IMonetaryPolicy + + +event Approval: + owner: address + spender: address + allow: bool + + +event SetExtraHealth: + user: address + health: uint256 + + +event CollectFees: + amount: uint256 + new_supply: uint256 + + +event SetAdminPercentage: + admin_percentage: uint256 + + +# Functions + +@view +@external +def monetary_policy() -> IMonetaryPolicy: + ... + + +@view +@external +def max_borrowable(collateral: uint256, N: uint256, current_debt: uint256, user: address) -> uint256: + ... + + +@external +def liquidate(user: address, min_x: uint256, _frac: uint256, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def borrow_more(collateral: uint256, debt: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def set_callback(cb: ILMGauge): + ... + + +@external +def set_borrowing_discounts(loan_discount: uint256, liquidation_discount: uint256): + ... + + +@external +def set_monetary_policy(monetary_policy: IMonetaryPolicy): + ... + + +@external +def create_loan(collateral: uint256, debt: uint256, N: uint256, _for: address, callbacker: address, calldata: Bytes[10000]): + ... + + +@external +def set_amm_fee(fee: uint256): + ... + + +@external +def repay( + _d_debt: uint256, + _for: address = msg.sender, + max_active_band: int256 = max_value(int256), + callbacker: address = empty(address), + calldata: Bytes[10000] = b"", + shrink: bool = False, +): + ... + + +@external +def approve(_spender: address, _allow: bool): + ... + + +@external +def set_extra_health(_value: uint256): + ... + + +@external +def save_rate(): + ... + + +@external +def collect_fees() -> uint256: + ... + + +@external +def add_collateral(collateral: uint256, _for: address): + ... + + +@external +def remove_collateral(collateral: uint256, _for: address): + ... + + +@view +@external +def amm() -> IAMM: + ... + + +@view +@external +def collateral_token() -> IERC20: + ... + + +@view +@external +def borrowed_token() -> IERC20: + ... + + +@view +@external +def debt(user: address) -> uint256: + ... + + +@view +@external +def loan_exists(user: address) -> bool: + ... + + +@view +@external +def total_debt() -> uint256: + ... + + +@view +@external +def min_collateral(debt: uint256, N: uint256, user: address) -> uint256: + ... + + +@view +@external +def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, user: address) -> int256: + ... + + +@view +@external +def user_prices(user: address) -> uint256[2]: + ... + + +@view +@external +def amm_price() -> uint256: + ... + + +@view +@external +def user_state(user: address) -> uint256[4]: + ... + + +@view +@external +def health_calculator(user: address, d_collateral: int256, d_debt: int256, full: bool, N: uint256) -> int256: + ... + + +@view +@external +def tokens_to_liquidate(user: address, frac: uint256) -> uint256: + ... + + +@view +@external +def health(user: address, full: bool) -> int256: + ... + + +@view +@external +def users_to_liquidate(_from: uint256, _limit: uint256) -> DynArray[IController.Position, 1000]: + ... + + +@view +@external +def admin_fees() -> uint256: + ... + + +@view +@external +def factory() -> IFactory: + ... + + +@view +@external +def liquidation_discount() -> uint256: + ... + + +@view +@external +def loan_discount() -> uint256: + ... + + +@view +@external +def approval(arg0: address, arg1: address) -> bool: + ... + + +@view +@external +def extra_health(arg0: address) -> uint256: + ... + + +@view +@external +def liquidation_discounts(arg0: address) -> uint256: + ... + + +@view +@external +def loans(arg0: uint256) -> address: + ... + + +@view +@external +def loan_ix(arg0: address) -> uint256: + ... + + +@view +@external +def n_loans() -> uint256: + ... + +# EXTRA THINGS FOR LLAMALEND CONTROLLER + +event SetBorrowCap: + borrow_cap: uint256 + + +@view +@external +def vault() -> address: + ... + + +@external +def set_borrow_cap(_borrow_cap: uint256): + ... + + +@external +def set_admin_percentage(admin_percentage: uint256): + ... + + +@view +@external +def collected() -> uint256: + ... + + +@view +@external +def admin_percentage() -> uint256: + ... + + +@view +@external +def available_balance() -> uint256: + ... + + +@external +@view +def borrow_cap() -> uint256: + ... diff --git a/contracts/interfaces/IMonetaryPolicy.vyi b/contracts/interfaces/IMonetaryPolicy.vyi new file mode 100644 index 00000000..932896cb --- /dev/null +++ b/contracts/interfaces/IMonetaryPolicy.vyi @@ -0,0 +1,6 @@ +def rate_write(_for: address = msg.sender) -> uint256: + ... + +@view +def rate(_for: address = msg.sender) -> uint256: + ... diff --git a/contracts/interfaces/IPartialRepayZap.vyi b/contracts/interfaces/IPartialRepayZap.vyi new file mode 100644 index 00000000..7501e064 --- /dev/null +++ b/contracts/interfaces/IPartialRepayZap.vyi @@ -0,0 +1,41 @@ +from contracts.interfaces import IController + + +event PartialRepay: + controller: IController + user: address + collateral_decrease: uint256 + borrowed_from_sender: uint256 + surplus_repaid: uint256 + + +struct Position: + user: address + x: uint256 + y: uint256 + health: int256 + dx: uint256 # estimated collateral out for FRAC + dy: uint256 # borrowed needed from sender for FRAC + + +@view +@external +def users_to_liquidate(_controller: IController, _from: uint256, _limit: uint256) -> DynArray[Position, 1000]: + ... + + +@external +def liquidate_partial(_controller: IController, _user: address, _min_x: uint256): + ... + + +@view +@external +def FRAC() -> uint256: + ... + + +@view +@external +def HEALTH_THRESHOLD() -> int256: + ... diff --git a/contracts/interfaces/IPartialRepayZapCallback.vyi b/contracts/interfaces/IPartialRepayZapCallback.vyi new file mode 100644 index 00000000..43384b5b --- /dev/null +++ b/contracts/interfaces/IPartialRepayZapCallback.vyi @@ -0,0 +1,44 @@ +from contracts.interfaces import IController + + +event PartialRepay: + controller: IController + user: address + surplus_repaid: uint256 + + +struct Position: + user: address + x: uint256 + y: uint256 + health: int256 + dx: uint256 # estimated collateral out for FRAC + dy: uint256 # borrowed needed from sender for FRAC + + +@view +@external +def users_to_liquidate(_controller: IController, _from: uint256, _limit: uint256) -> DynArray[Position, 1000]: + ... + + +@external +def liquidate_partial( + _controller: IController, + _user: address, _min_x: uint256, + _callbacker: address = empty(address), + _calldata: Bytes[10 ** 4 - 32 * 6 - 16] = b"", +): + ... + + +@view +@external +def FRAC() -> uint256: + ... + + +@view +@external +def HEALTH_THRESHOLD() -> int256: + ... diff --git a/contracts/interfaces/IPool.vyi b/contracts/interfaces/IPool.vyi new file mode 100644 index 00000000..d1368d10 --- /dev/null +++ b/contracts/interfaces/IPool.vyi @@ -0,0 +1,9 @@ +@view +@external +def price_oracle(i: uint256=0) -> uint256: + ... + +@view +@external +def coins(i: uint256) -> address: + ... diff --git a/contracts/interfaces/IPriceOracle.vyi b/contracts/interfaces/IPriceOracle.vyi new file mode 100644 index 00000000..d8d09916 --- /dev/null +++ b/contracts/interfaces/IPriceOracle.vyi @@ -0,0 +1,6 @@ +@view +def price() -> uint256: + ... + +def price_w() -> uint256: + ... diff --git a/contracts/interfaces/IStablePool.vyi b/contracts/interfaces/IStablePool.vyi new file mode 100644 index 00000000..65e1c5f9 --- /dev/null +++ b/contracts/interfaces/IStablePool.vyi @@ -0,0 +1,11 @@ +@view +def coins(i: uint256) -> address: + ... + +@view +def price_oracle(i: uint256 = 0) -> uint256: # Universal method! + ... + +@view +def get_virtual_price() -> uint256: + ... diff --git a/contracts/interfaces/IVault.vyi b/contracts/interfaces/IVault.vyi new file mode 100644 index 00000000..99485813 --- /dev/null +++ b/contracts/interfaces/IVault.vyi @@ -0,0 +1,45 @@ +from curve_std.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import ILlamalendController as IController + +@view +def collateral_token() -> IERC20: + ... + +@view +def borrowed_token() -> IERC20: + ... + +@view +def net_deposits() -> int256: + ... + +@view +def admin() -> address: + ... + +@view +def fee_receiver() -> address: + ... + +@external +def initialize(amm: IAMM, controller: address, borrowed_token: IERC20, collateral_token: IERC20): + ... + +@view +@external +def controller() -> IController: + ... + + +@view +@external +def amm() -> IAMM: + ... + + +@external +def set_max_supply(max_supply: uint256): + ... + +# TODO populate + implement diff --git a/contracts/lending/LendController.vy b/contracts/lending/LendController.vy new file mode 100644 index 00000000..a9b21fe6 --- /dev/null +++ b/contracts/lending/LendController.vy @@ -0,0 +1,254 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +# pragma optimize codesize +""" +@title LlamaLend Lend Market Controller +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice Main contract to interact with a Llamalend Lend Market. Each + contract is specific to a single mint market. +@custom:security security@curve.fi +""" + +from curve_std.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IVault +from contracts.interfaces import IController + +implements: IController + +from contracts.interfaces import ILlamalendController + +implements: ILlamalendController + +from contracts import Controller as core + +implements: core.VirtualMethods + +initializes: core + +from curve_std import token as tkn +from curve_std import math as crv_math + +exports: ( + # Loan management + core.create_loan, + core.borrow_more, + core.add_collateral, + core.approve, + core.remove_collateral, + core.repay, + core.set_extra_health, + core.liquidate, + core.save_rate, + core.collect_fees, + # Getters + core.amm, + core.amm_price, + core.approval, + core.borrowed_token, + core.calculate_debt_n1, + core.collateral_token, + core.debt, + core.extra_health, + core.health, + core.health_calculator, + core.liquidation_discount, + core.liquidation_discounts, + core.loan_discount, + core.loan_exists, + core.loan_ix, + core.loans, + core.monetary_policy, + core.n_loans, + core.tokens_to_liquidate, + core.tokens_to_shrink, + core.total_debt, + core.factory, + core.admin_fees, + core.admin_percentage, + # Setters + core.set_view, + core.set_amm_fee, + core.set_borrowing_discounts, + core.set_callback, + core.set_monetary_policy, + core.set_price_oracle, + # From view contract + core.user_prices, + core.user_state, + core.users_to_liquidate, + core.min_collateral, + core.max_borrowable, +) + +borrow_cap: public(uint256) +VAULT: immutable(IVault) + + +# https://github.com/vyperlang/vyper/issues/4721 +@external +@view +def vault() -> address: + """ + @notice Address of the vault + """ + return VAULT.address + + +@deploy +def __init__( + vault: IVault, + amm: IAMM, + borrowed_token: IERC20, + collateral_token: IERC20, + monetary_policy: IMonetaryPolicy, + loan_discount: uint256, + liquidation_discount: uint256, + view_impl: address, +): + """ + @notice Lend Controller constructor + @param collateral_token Token to use for collateral + @param monetary_policy Address of monetary policy + @param loan_discount Discount of the maximum loan size compare to get_x_down() value + @param liquidation_discount Discount of the maximum loan size compare to + get_x_down() for "bad liquidation" purposes + @param amm AMM address (Already deployed from blueprint) + """ + VAULT = vault + + core.__init__( + collateral_token, + borrowed_token, + monetary_policy, + loan_discount, + liquidation_discount, + amm, + view_impl, + ) + + # Borrow cap is zero by default in lend markets. The admin has to raise it + # after deployment to allow borrowing. + core.admin_percentage = 0 + + # Pre-approve the vault to transfer borrowed tokens out of the controller + tkn.max_approve(core.BORROWED_TOKEN, VAULT.address) + + +@external +@view +def version() -> String[10]: + """ + @notice Version of this controller + """ + return concat(core.version, "-lend") + + +@view +@external +def available_balance() -> uint256: + """ + @notice Amount of borrowed token the vault can use for withdrawals or new loans + @dev Used by the vault for its accounting logic to ignore tokens sent directly to the controller. + """ + return self._available_balance() + + +@internal +@view +def _available_balance() -> uint256: + # This functions provides the balance of tokens in the controller + # that can be withdrawn or used for new loans at any time. + + # This is required because a naive measure like balanceOf can easily + # be manipulated making it easy to inflate the vault balance. + + # Any amount sent directly to this controller contract is lost forever. + + # TODO rewrite this comment + available_balance: int256 = staticcall VAULT.net_deposits() + + # We compute the outstanding amount that has been lent out but not yet repaid + outstanding: int256 = convert(core.lent, int256) - convert(core.repaid, int256) + + # We deduce outstanding balance from the available balance + available_balance -= outstanding + + # TODO make sure this can NEVER happen through testing + assert available_balance >= 0 # dev: sanity check + + # We compute the total admin fees combining fees that have already been + # collected and fees that still live in the controller's balance + total_admin_fees: uint256 = core.collected + core.admin_fees + + available_balance -= convert(total_admin_fees, int256) + + if available_balance < 0: + return 0 + return convert(available_balance, uint256) + + +@external +def set_borrow_cap(_borrow_cap: uint256): + """ + @notice Set the borrow cap for this market + @dev Only callable by the factory admin + @param _borrow_cap New borrow cap in units of borrowed_token + """ + core._check_admin() + self.borrow_cap = _borrow_cap + log ILlamalendController.SetBorrowCap(borrow_cap=_borrow_cap) + + +@external +def set_admin_percentage(_admin_percentage: uint256): + """ + @param _admin_percentage The percentage of interest that goes to the admin, scaled by 1e18 + """ + core._check_admin() + assert _admin_percentage <= core.WAD # dev: admin percentage higher than 100% + core.admin_percentage = _admin_percentage + log ILlamalendController.SetAdminPercentage(admin_percentage=_admin_percentage) + + +@external +@reentrant +def _on_debt_increased(delta: uint256, total_debt: uint256): + """ + @notice Hook called when debt is increased + """ + assert msg.sender == self # dev: virtual method protection + # TODO move to solidity style errors + assert total_debt <= self.borrow_cap, "Borrow cap exceeded" + assert delta <= self._available_balance(), "Available balance exceeded" + + +@external +@view +def lent() -> uint256: + """ + @notice Total amount of borrowed tokens lent out since creation + @return Total lent amount + """ + return core.lent + + +@external +@view +def repaid() -> uint256: + """ + @notice Total amount of borrowed tokens repaid since creation + @return Total repaid amount + """ + return core.repaid + + +@external +@view +def collected() -> uint256: + """ + @notice Cumulative amount of borrowed assets ever collected as admin fees + """ + return core.collected diff --git a/contracts/lending/LendControllerView.vy b/contracts/lending/LendControllerView.vy new file mode 100644 index 00000000..dd993405 --- /dev/null +++ b/contracts/lending/LendControllerView.vy @@ -0,0 +1,96 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +""" +@title Llamalend Controller View Contract +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice This contract never requires any direct interaction as the + main controller contract forwards all relevant calls. +""" + +from curve_std.interfaces import IERC20 +from contracts.interfaces import IController +from contracts.interfaces import ILlamalendController +from contracts.interfaces import IAMM +from contracts.interfaces import IControllerView + +implements: IControllerView + +from contracts import ControllerView as core + +initializes: core +exports: ( + core.min_collateral, + core.user_state, + core.user_prices, + core.users_to_liquidate, + core.health_calculator, +) + + +@deploy +def __init__( + _controller: IController, + _sqrt_band_ratio: uint256, + _logn_a_ratio: int256, + _amm: IAMM, + _A: uint256, + _collateral_token: IERC20, + _collateral_precision: uint256, + _borrowed_token: IERC20, + _borrowed_precision: uint256 +): + core.__init__( + _controller, + _sqrt_band_ratio, + _logn_a_ratio, + _amm, + _A, + _collateral_token, + _collateral_precision, + _borrowed_token, + _borrowed_precision + ) + + +@internal +@view +def _total_debt() -> uint256: + return staticcall core.CONTROLLER.total_debt() + + +@internal +@view +def _borrow_cap() -> uint256: + ll_core: ILlamalendController = ILlamalendController( + core.CONTROLLER.address + ) + return staticcall ll_core.borrow_cap() + + +@internal +@view +def _available_balance() -> uint256: + ll_core: ILlamalendController = ILlamalendController( + core.CONTROLLER.address + ) + return staticcall ll_core.available_balance() + + +@external +@view +def max_borrowable( + collateral: uint256, + N: uint256, + current_debt: uint256 = 0, + user: address = empty(address), +) -> uint256: + """ + @notice Natspec for this function is available in its controller contract + """ + # Cannot borrow beyond the amount of coins Controller has or beyond borrow_cap + total_debt: uint256 = self._total_debt() + cap: uint256 = unsafe_sub(max(self._borrow_cap(), total_debt), total_debt) + cap = min(self._available_balance(), cap) + current_debt + + return core._max_borrowable(collateral, N, cap, user) diff --git a/contracts/lending/LendingFactory.vy b/contracts/lending/LendingFactory.vy new file mode 100644 index 00000000..e87dadbd --- /dev/null +++ b/contracts/lending/LendingFactory.vy @@ -0,0 +1,289 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +""" +@title LlamaLend Factory +@notice Factory for one-way lending markets powered by LlamaLend AMM +@author Curve.fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@custom:security security@curve.fi +""" + +from curve_std.interfaces import IERC20 + +# TODO make module friendly +from contracts.interfaces import IVault +from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IAMM +from contracts.interfaces import IPriceOracle +from contracts.interfaces import ILendingFactory +from contracts.interfaces import IMonetaryPolicy + +implements: ILendingFactory + +from snekmate.utils import math +from snekmate.auth import ownable + +initializes: ownable + +exports: ( + # owner is not exported as we refer to it as `admin` for backwards compatibility + ownable.renounce_ownership, + ownable.transfer_ownership, +) + + +# TODO move elsewhere +struct Market: + vault: IVault + controller: IController + amm: IAMM + collateral_token: IERC20 + borrowed_token: IERC20 + price_oracle: IPriceOracle + monetary_policy: IMonetaryPolicy + + +MIN_A: constant(uint256) = 2 +MAX_A: constant(uint256) = 10000 +MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 +MAX_FEE: constant(uint256) = 10**17 # 10% +MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 +MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 + +# Implementations which can be changed by governance +amm_blueprint: public(address) +controller_blueprint: public(address) +# convert to blueprint +vault_blueprint: public(address) +controller_view_blueprint: public(address) + +# Admin is supposed to be the DAO +fee_receiver: public(address) + +# Vaults can only be created but not removed +vaults: public(IVault[10**18]) +_vaults_index: HashMap[IVault, uint256] +market_count: public(uint256) + +names: public(HashMap[uint256, String[64]]) + + +@deploy +def __init__( + _amm_blueprint: address, + _controller_blueprint: address, + _vault_blueprint: address, + _pool_price_oracle_blueprint: address, + _controller_view_blueprint: address, + _admin: address, + _fee_receiver: address, +): + """ + @notice Factory which creates one-way lending vaults (e.g. collateral is non-borrowable) + @param amm Address of AMM implementation + @param controller Address of Controller implementation + @param pool_price_oracle Address of implementation for price oracle factory (prices from pools) + @param admin Admin address (DAO) + @param fee_receiver Receiver of interest and admin fees + """ + self.amm_blueprint = _amm_blueprint + self.controller_blueprint = _controller_blueprint + self.vault_blueprint = _vault_blueprint + self.controller_view_blueprint = _controller_view_blueprint + + ownable.__init__() + ownable._transfer_ownership(_admin) + + self.fee_receiver = _fee_receiver + + +@external +def create( + _borrowed_token: IERC20, + _collateral_token: IERC20, + _A: uint256, + _fee: uint256, + _loan_discount: uint256, + _liquidation_discount: uint256, + _price_oracle: IPriceOracle, + _monetary_policy: IMonetaryPolicy, + _name: String[64], + _supply_limit: uint256, +) -> address[3]: + """ + @notice Creation of the vault using user-supplied price oracle contract + @param _borrowed_token Token which is being borrowed + @param _collateral_token Token used for collateral + @param _A Amplification coefficient: band size is ~1//A + @param _fee Fee for swaps in AMM (for ETH markets found to be 0.6%) + @param _loan_discount Maximum discount. LTV = sqrt(((A - 1) // A) ** 4) - loan_discount + @param _liquidation_discount Liquidation discount. LT = sqrt(((A - 1) // A) ** 4) - liquidation_discount + @param _price_oracle Custom price oracle contract + @param _name Human-readable market name + @param _supply_limit Supply cap + """ + assert _borrowed_token != _collateral_token, "Same token" + assert _A >= MIN_A and _A <= MAX_A, "Wrong A" + assert _fee <= MAX_FEE, "Fee too high" + assert _fee >= MIN_FEE, "Fee too low" + assert (_liquidation_discount >= MIN_LIQUIDATION_DISCOUNT), "Liquidation discount too low" + assert _loan_discount <= MAX_LOAN_DISCOUNT, "Loan discount too high" + assert (_loan_discount > _liquidation_discount), "need loan_discount>liquidation_discount" + + A_ratio: uint256 = 10**18 * _A // (_A - 1) + + # Validate price oracle + p: uint256 = (staticcall _price_oracle.price()) + assert p > 0 + assert extcall _price_oracle.price_w() == p + + vault: IVault = IVault(create_from_blueprint(self.vault_blueprint)) + amm: IAMM = IAMM( + create_from_blueprint( + self.amm_blueprint, + _borrowed_token, + 10**convert(18 - staticcall _borrowed_token.decimals(), uint256), + _collateral_token, + 10**convert(18 - staticcall _collateral_token.decimals(), uint256), + _A, + isqrt(A_ratio * 10**18), + math._wad_ln(convert(A_ratio, int256)), + p, + _fee, + convert(0, uint256), + _price_oracle, + code_offset=3, + ) + ) + controller: IController = IController( + create_from_blueprint( + self.controller_blueprint, + vault, + amm, + _borrowed_token, + _collateral_token, + _monetary_policy, + _loan_discount, + _liquidation_discount, + self.controller_view_blueprint, + code_offset=3, + ) + ) + extcall amm.set_admin(controller.address) + + extcall vault.initialize(amm, controller.address, _borrowed_token, _collateral_token) + + # Validate monetary policy using controller context + extcall _monetary_policy.rate_write(controller.address) + + market_count: uint256 = self.market_count + log ILendingFactory.NewVault( + id=market_count, + collateral_token=_collateral_token, + borrowed_token=_borrowed_token, + vault=vault, + controller=controller, + amm=amm, + price_oracle=_price_oracle, + monetary_policy=_monetary_policy, + ) + self.vaults[market_count] = vault + # Store index with 2**128 offset so missing vault lookups revert (e.g. nonexistent vault would otherwise read index 0) + self._vaults_index[vault] = market_count + 2**128 + self.names[market_count] = _name + self.market_count = market_count + 1 + + if _supply_limit < max_value(uint256): + extcall vault.set_max_supply(_supply_limit) + + return [vault.address, controller.address, amm.address] + + +@external +@view +@reentrant +def markets(_n: uint256) -> Market: + vault: IVault = self.vaults[_n] + controller: IController = staticcall vault.controller() + amm: IAMM = staticcall vault.amm() + + return Market( + vault=vault, + controller=controller, + amm=amm, + collateral_token=staticcall vault.collateral_token(), + borrowed_token=staticcall vault.borrowed_token(), + price_oracle=staticcall amm.price_oracle_contract(), + monetary_policy=staticcall controller.monetary_policy(), + ) + + +@external +@view +@reentrant +def vaults_index(_vault: IVault) -> uint256: + return self._vaults_index[_vault] - 2**128 + + +@external +def set_implementations( + _controller_blueprint: address, + _amm_blueprint: address, + _vault_blueprint: address, + _controller_view_blueprint: address, +): + """ + @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary policy. + Doesn't change existing ones + @param _controller_blueprint Address of the controller blueprint + @param _amm_blueprint Address of the AMM blueprint + @param _vault_blueprint Address of the Vault blueprint + @param _controller_view_blueprint Address of the view contract blueprint + """ + ownable._check_owner() + + if _controller_blueprint != empty(address): + self.controller_blueprint = _controller_blueprint + if _amm_blueprint != empty(address): + self.amm_blueprint = _amm_blueprint + if _vault_blueprint != empty(address): + self.vault_blueprint = _vault_blueprint + if _controller_view_blueprint != empty(address): + self.controller_view_blueprint = _controller_view_blueprint + + log ILendingFactory.SetBlueprints( + amm=_amm_blueprint, + controller=_controller_blueprint, + vault=_vault_blueprint, + controller_view=_controller_view_blueprint, + ) + + +@external +@view +@reentrant +def admin() -> address: + """ + @notice Get the admin of the factory + """ + return ownable.owner + + +@external +def set_fee_receiver(_fee_receiver: address): + """ + @notice Set fee receiver who earns interest (DAO) + @param _fee_receiver Address of the receiver + """ + ownable._check_owner() + assert _fee_receiver != empty(address) + self.fee_receiver = _fee_receiver + log ILendingFactory.SetFeeReceiver(fee_receiver=_fee_receiver) + + +@external +@view +@reentrant +def coins(_vault_id: uint256) -> IERC20[2]: + vault: IVault = self.vaults[_vault_id] + return [staticcall vault.borrowed_token(), staticcall vault.collateral_token()] diff --git a/contracts/lending/LiquidityGauge.vy b/contracts/lending/LiquidityGauge.vy deleted file mode 100644 index 8228ecec..00000000 --- a/contracts/lending/LiquidityGauge.vy +++ /dev/null @@ -1,864 +0,0 @@ -# pragma version 0.3.10 -# pragma optimize gas -# pragma evm-version shanghai -""" -@title LiquidityGaugeV6 -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Implementation contract for use with Curve Factory -@dev Differs from v5.0.0 in that it uses create_from_blueprint to deploy Gauges -""" -from vyper.interfaces import ERC20 - -implements: ERC20 - - -interface CRV20: - def future_epoch_time_write() -> uint256: nonpayable - def rate() -> uint256: view - -interface Controller: - def checkpoint_gauge(addr: address): nonpayable - def gauge_relative_weight(addr: address, time: uint256) -> uint256: view - -interface ERC20Extended: - def symbol() -> String[32]: view - -interface ERC1271: - def isValidSignature(_hash: bytes32, _signature: Bytes[65]) -> bytes32: view - -interface Factory: - def admin() -> address: view - -interface Minter: - def minted(user: address, gauge: address) -> uint256: view - -interface VotingEscrow: - def user_point_epoch(addr: address) -> uint256: view - def user_point_history__ts(addr: address, epoch: uint256) -> uint256: view - -interface VotingEscrowBoost: - def adjusted_balance_of(_account: address) -> uint256: view - - -event Deposit: - provider: indexed(address) - value: uint256 - -event Withdraw: - provider: indexed(address) - value: uint256 - -event UpdateLiquidityLimit: - user: indexed(address) - original_balance: uint256 - original_supply: uint256 - working_balance: uint256 - working_supply: uint256 - -event CommitOwnership: - admin: address - -event ApplyOwnership: - admin: address - -event SetGaugeManager: - _gauge_manager: address - - -event Transfer: - _from: indexed(address) - _to: indexed(address) - _value: uint256 - -event Approval: - _owner: indexed(address) - _spender: indexed(address) - _value: uint256 - - -struct Reward: - token: address - distributor: address - period_finish: uint256 - rate: uint256 - last_update: uint256 - integral: uint256 - - -MAX_REWARDS: constant(uint256) = 8 -TOKENLESS_PRODUCTION: constant(uint256) = 40 -WEEK: constant(uint256) = 604800 - -VERSION: constant(String[8]) = "v6.1.0" # <- updated from v6.0.0 (makes rewards semi-permissionless) - -EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") -EIP2612_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") - -VERSION_HASH: constant(bytes32) = keccak256(VERSION) -NAME_HASH: immutable(bytes32) -CACHED_CHAIN_ID: immutable(uint256) -salt: public(immutable(bytes32)) -CACHED_DOMAIN_SEPARATOR: immutable(bytes32) - -CRV: constant(address) = 0xD533a949740bb3306d119CC777fa900bA034cd52 -GAUGE_CONTROLLER: constant(address) = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB -MINTER: constant(address) = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0 -VEBOOST_PROXY: constant(address) = 0x8E0c00ed546602fD9927DF742bbAbF726D5B0d16 -VOTING_ESCROW: constant(address) = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2 - - -# ERC20 -balanceOf: public(HashMap[address, uint256]) -totalSupply: public(uint256) -allowance: public(HashMap[address, HashMap[address, uint256]]) - -name: public(String[64]) -symbol: public(String[40]) - -# ERC2612 -nonces: public(HashMap[address, uint256]) - -# Gauge -factory: public(address) -manager: public(address) -lp_token: public(address) - -is_killed: public(bool) - -# [future_epoch_time uint40][inflation_rate uint216] -inflation_params: uint256 - -# For tracking external rewards -reward_count: public(uint256) -reward_data: public(HashMap[address, Reward]) - -# claimant -> default reward receiver -rewards_receiver: public(HashMap[address, address]) - -# reward token -> claiming address -> integral -reward_integral_for: public(HashMap[address, HashMap[address, uint256]]) - -# user -> [uint128 claimable amount][uint128 claimed amount] -claim_data: HashMap[address, HashMap[address, uint256]] - -working_balances: public(HashMap[address, uint256]) -working_supply: public(uint256) - -# 1e18 * ∫(rate(t) / totalSupply(t) dt) from (last_action) till checkpoint -integrate_inv_supply_of: public(HashMap[address, uint256]) -integrate_checkpoint_of: public(HashMap[address, uint256]) - -# ∫(balance * rate(t) / totalSupply(t) dt) from 0 till checkpoint -# Units: rate * t = already number of coins per address to issue -integrate_fraction: public(HashMap[address, uint256]) - -# The goal is to be able to calculate ∫(rate * balance / totalSupply dt) from 0 till checkpoint -# All values are kept in units of being multiplied by 1e18 -period: public(int128) - -# array of reward tokens -reward_tokens: public(address[MAX_REWARDS]) - -period_timestamp: public(uint256[100000000000000000000000000000]) -# 1e18 * ∫(rate(t) / totalSupply(t) dt) from 0 till checkpoint -integrate_inv_supply: public(uint256[100000000000000000000000000000]) # bump epoch when rate() changes - - -@external -def __init__(_lp_token: address): - """ - @notice Contract constructor - @param _lp_token Liquidity Pool contract address - """ - self.lp_token = _lp_token - self.factory = msg.sender - self.manager = tx.origin - - symbol: String[32] = ERC20Extended(_lp_token).symbol() - name: String[64] = concat("Curve.fi ", symbol, " Gauge Deposit") - - self.name = name - self.symbol = concat(symbol, "-gauge") - - self.period_timestamp[0] = block.timestamp - self.inflation_params = ( - (CRV20(CRV).future_epoch_time_write() << 216) - + CRV20(CRV).rate() - ) - - NAME_HASH = keccak256(name) - salt = block.prevhash - CACHED_CHAIN_ID = chain.id - CACHED_DOMAIN_SEPARATOR = keccak256( - _abi_encode( - EIP712_TYPEHASH, - NAME_HASH, - VERSION_HASH, - chain.id, - self, - salt, - ) - ) - - -# Internal Functions - -@view -@internal -def _domain_separator() -> bytes32: - if chain.id != CACHED_CHAIN_ID: - return keccak256( - _abi_encode( - EIP712_TYPEHASH, - NAME_HASH, - VERSION_HASH, - chain.id, - self, - salt, - ) - ) - return CACHED_DOMAIN_SEPARATOR - - -@internal -def _checkpoint(addr: address): - """ - @notice Checkpoint for a user - @dev Updates the CRV emissions a user is entitled to receive - @param addr User address - """ - _period: int128 = self.period - _period_time: uint256 = self.period_timestamp[_period] - _integrate_inv_supply: uint256 = self.integrate_inv_supply[_period] - - inflation_params: uint256 = self.inflation_params - prev_future_epoch: uint256 = inflation_params >> 216 - gauge_is_killed: bool = self.is_killed - - rate: uint256 = inflation_params % 2 ** 216 - new_rate: uint256 = rate - if gauge_is_killed: - rate = 0 - new_rate = 0 - - if prev_future_epoch >= _period_time: - future_epoch_time_write: uint256 = CRV20(CRV).future_epoch_time_write() - if not gauge_is_killed: - new_rate = CRV20(CRV).rate() - self.inflation_params = (future_epoch_time_write << 216) + new_rate - - # Update integral of 1/supply - if block.timestamp > _period_time: - _working_supply: uint256 = self.working_supply - Controller(GAUGE_CONTROLLER).checkpoint_gauge(self) - prev_week_time: uint256 = _period_time - week_time: uint256 = min((_period_time + WEEK) / WEEK * WEEK, block.timestamp) - - for i in range(500): - dt: uint256 = week_time - prev_week_time - w: uint256 = Controller(GAUGE_CONTROLLER).gauge_relative_weight(self, prev_week_time) - - if _working_supply > 0: - if prev_future_epoch >= prev_week_time and prev_future_epoch < week_time: - # If we went across one or multiple epochs, apply the rate - # of the first epoch until it ends, and then the rate of - # the last epoch. - # If more than one epoch is crossed - the gauge gets less, - # but that'd meen it wasn't called for more than 1 year - _integrate_inv_supply += rate * w * (prev_future_epoch - prev_week_time) / _working_supply - rate = new_rate - _integrate_inv_supply += rate * w * (week_time - prev_future_epoch) / _working_supply - else: - _integrate_inv_supply += rate * w * dt / _working_supply - # On precisions of the calculation - # rate ~= 10e18 - # last_weight > 0.01 * 1e18 = 1e16 (if pool weight is 1%) - # _working_supply ~= TVL * 1e18 ~= 1e26 ($100M for example) - # The largest loss is at dt = 1 - # Loss is 1e-9 - acceptable - - if week_time == block.timestamp: - break - prev_week_time = week_time - week_time = min(week_time + WEEK, block.timestamp) - - _period += 1 - self.period = _period - self.period_timestamp[_period] = block.timestamp - self.integrate_inv_supply[_period] = _integrate_inv_supply - - # Update user-specific integrals - _working_balance: uint256 = self.working_balances[addr] - self.integrate_fraction[addr] += _working_balance * (_integrate_inv_supply - self.integrate_inv_supply_of[addr]) / 10 ** 18 - self.integrate_inv_supply_of[addr] = _integrate_inv_supply - self.integrate_checkpoint_of[addr] = block.timestamp - - -@internal -def _checkpoint_rewards(_user: address, _total_supply: uint256, _claim: bool, _receiver: address): - """ - @notice Claim pending rewards and checkpoint rewards for a user - """ - - user_balance: uint256 = 0 - receiver: address = _receiver - if _user != empty(address): - user_balance = self.balanceOf[_user] - if _claim and _receiver == empty(address): - # if receiver is not explicitly declared, check if a default receiver is set - receiver = self.rewards_receiver[_user] - if receiver == empty(address): - # if no default receiver is set, direct claims to the user - receiver = _user - - reward_count: uint256 = self.reward_count - for i in range(MAX_REWARDS): - if i == reward_count: - break - token: address = self.reward_tokens[i] - - integral: uint256 = self.reward_data[token].integral - last_update: uint256 = min(block.timestamp, self.reward_data[token].period_finish) - duration: uint256 = last_update - self.reward_data[token].last_update - - if duration != 0 and _total_supply != 0: - self.reward_data[token].last_update = last_update - integral += duration * self.reward_data[token].rate * 10**18 / _total_supply - self.reward_data[token].integral = integral - - if _user != empty(address): - integral_for: uint256 = self.reward_integral_for[token][_user] - new_claimable: uint256 = 0 - - if integral_for < integral: - self.reward_integral_for[token][_user] = integral - new_claimable = user_balance * (integral - integral_for) / 10**18 - - claim_data: uint256 = self.claim_data[_user][token] - total_claimable: uint256 = (claim_data >> 128) + new_claimable - if total_claimable > 0: - total_claimed: uint256 = claim_data % 2**128 - if _claim: - assert ERC20(token).transfer(receiver, total_claimable, default_return_value=True) - self.claim_data[_user][token] = total_claimed + total_claimable - elif new_claimable > 0: - self.claim_data[_user][token] = total_claimed + (total_claimable << 128) - - -@internal -def _update_liquidity_limit(addr: address, l: uint256, L: uint256): - """ - @notice Calculate limits which depend on the amount of CRV token per-user. - Effectively it calculates working balances to apply amplification - of CRV production by CRV - @param addr User address - @param l User's amount of liquidity (LP tokens) - @param L Total amount of liquidity (LP tokens) - """ - # To be called after totalSupply is updated - voting_balance: uint256 = VotingEscrowBoost(VEBOOST_PROXY).adjusted_balance_of(addr) - voting_total: uint256 = ERC20(VOTING_ESCROW).totalSupply() - - lim: uint256 = l * TOKENLESS_PRODUCTION / 100 - if voting_total > 0: - lim += L * voting_balance / voting_total * (100 - TOKENLESS_PRODUCTION) / 100 - - lim = min(l, lim) - old_bal: uint256 = self.working_balances[addr] - self.working_balances[addr] = lim - _working_supply: uint256 = self.working_supply + lim - old_bal - self.working_supply = _working_supply - - log UpdateLiquidityLimit(addr, l, L, lim, _working_supply) - - -@internal -def _transfer(_from: address, _to: address, _value: uint256): - """ - @notice Transfer tokens as well as checkpoint users - """ - self._checkpoint(_from) - self._checkpoint(_to) - - if _value != 0: - total_supply: uint256 = self.totalSupply - is_rewards: bool = self.reward_count != 0 - if is_rewards: - self._checkpoint_rewards(_from, total_supply, False, empty(address)) - new_balance: uint256 = self.balanceOf[_from] - _value - self.balanceOf[_from] = new_balance - self._update_liquidity_limit(_from, new_balance, total_supply) - - if is_rewards: - self._checkpoint_rewards(_to, total_supply, False, empty(address)) - new_balance = self.balanceOf[_to] + _value - self.balanceOf[_to] = new_balance - self._update_liquidity_limit(_to, new_balance, total_supply) - - log Transfer(_from, _to, _value) - - -# External User Facing Functions - - -@external -@nonreentrant('lock') -def deposit(_value: uint256, _addr: address = msg.sender, _claim_rewards: bool = False): - """ - @notice Deposit `_value` LP tokens - @dev Depositting also claims pending reward tokens - @param _value Number of tokens to deposit - @param _addr Address to deposit for - """ - assert _addr != empty(address) # dev: cannot deposit for zero address - self._checkpoint(_addr) - - if _value != 0: - is_rewards: bool = self.reward_count != 0 - total_supply: uint256 = self.totalSupply - if is_rewards: - self._checkpoint_rewards(_addr, total_supply, _claim_rewards, empty(address)) - - total_supply += _value - new_balance: uint256 = self.balanceOf[_addr] + _value - self.balanceOf[_addr] = new_balance - self.totalSupply = total_supply - - self._update_liquidity_limit(_addr, new_balance, total_supply) - - ERC20(self.lp_token).transferFrom(msg.sender, self, _value) - - log Deposit(_addr, _value) - log Transfer(empty(address), _addr, _value) - - -@external -@nonreentrant('lock') -def withdraw(_value: uint256, _claim_rewards: bool = False): - """ - @notice Withdraw `_value` LP tokens - @dev Withdrawing also claims pending reward tokens - @param _value Number of tokens to withdraw - """ - self._checkpoint(msg.sender) - - if _value != 0: - is_rewards: bool = self.reward_count != 0 - total_supply: uint256 = self.totalSupply - if is_rewards: - self._checkpoint_rewards(msg.sender, total_supply, _claim_rewards, empty(address)) - - total_supply -= _value - new_balance: uint256 = self.balanceOf[msg.sender] - _value - self.balanceOf[msg.sender] = new_balance - self.totalSupply = total_supply - - self._update_liquidity_limit(msg.sender, new_balance, total_supply) - - ERC20(self.lp_token).transfer(msg.sender, _value) - - log Withdraw(msg.sender, _value) - log Transfer(msg.sender, empty(address), _value) - - -@external -@nonreentrant('lock') -def claim_rewards(_addr: address = msg.sender, _receiver: address = empty(address)): - """ - @notice Claim available reward tokens for `_addr` - @param _addr Address to claim for - @param _receiver Address to transfer rewards to - if set to - empty(address), uses the default reward receiver - for the caller - """ - if _receiver != empty(address): - assert _addr == msg.sender # dev: cannot redirect when claiming for another user - self._checkpoint_rewards(_addr, self.totalSupply, True, _receiver) - - -@external -@nonreentrant('lock') -def transferFrom(_from: address, _to :address, _value: uint256) -> bool: - """ - @notice Transfer tokens from one address to another. - @dev Transferring claims pending reward tokens for the sender and receiver - @param _from address The address which you want to send tokens from - @param _to address The address which you want to transfer to - @param _value uint256 the amount of tokens to be transferred - """ - _allowance: uint256 = self.allowance[_from][msg.sender] - if _allowance != max_value(uint256): - self.allowance[_from][msg.sender] = _allowance - _value - - self._transfer(_from, _to, _value) - - return True - - -@external -@nonreentrant('lock') -def transfer(_to: address, _value: uint256) -> bool: - """ - @notice Transfer token for a specified address - @dev Transferring claims pending reward tokens for the sender and receiver - @param _to The address to transfer to. - @param _value The amount to be transferred. - """ - self._transfer(msg.sender, _to, _value) - - return True - - -@external -def approve(_spender : address, _value : uint256) -> bool: - """ - @notice Approve the passed address to transfer the specified amount of - tokens on behalf of msg.sender - @dev Beware that changing an allowance via this method brings the risk - that someone may use both the old and new allowance by unfortunate - transaction ordering. This may be mitigated with the use of - {incraseAllowance} and {decreaseAllowance}. - https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - @param _spender The address which will transfer the funds - @param _value The amount of tokens that may be transferred - @return bool success - """ - self.allowance[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) - - return True - - -@external -def permit( - _owner: address, - _spender: address, - _value: uint256, - _deadline: uint256, - _v: uint8, - _r: bytes32, - _s: bytes32 -) -> bool: - """ - @notice Approves spender by owner's signature to expend owner's tokens. - See https://eips.ethereum.org/EIPS/eip-2612. - @dev Inspired by https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L753-L793 - @dev Supports smart contract wallets which implement ERC1271 - https://eips.ethereum.org/EIPS/eip-1271 - @param _owner The address which is a source of funds and has signed the Permit. - @param _spender The address which is allowed to spend the funds. - @param _value The amount of tokens to be spent. - @param _deadline The timestamp after which the Permit is no longer valid. - @param _v The bytes[64] of the valid secp256k1 signature of permit by owner - @param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner - @param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner - @return True, if transaction completes successfully - """ - assert _owner != empty(address) # dev: invalid owner - assert block.timestamp <= _deadline # dev: permit expired - - nonce: uint256 = self.nonces[_owner] - digest: bytes32 = keccak256( - concat( - b"\x19\x01", - self._domain_separator(), - keccak256( - _abi_encode( - EIP2612_TYPEHASH, _owner, _spender, _value, nonce, _deadline - ) - ), - ) - ) - assert ecrecover(digest, _v, _r, _s) == _owner # dev: invalid signature - - self.allowance[_owner][_spender] = _value - self.nonces[_owner] = unsafe_add(nonce, 1) - - log Approval(_owner, _spender, _value) - return True - - -@external -def increaseAllowance(_spender: address, _added_value: uint256) -> bool: - """ - @notice Increase the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition - @param _spender The address which will transfer the funds - @param _added_value The amount of to increase the allowance - @return bool success - """ - allowance: uint256 = self.allowance[msg.sender][_spender] + _added_value - self.allowance[msg.sender][_spender] = allowance - - log Approval(msg.sender, _spender, allowance) - - return True - - -@external -def decreaseAllowance(_spender: address, _subtracted_value: uint256) -> bool: - """ - @notice Decrease the allowance granted to `_spender` by the caller - @dev This is alternative to {approve} that can be used as a mitigation for - the potential race condition - @param _spender The address which will transfer the funds - @param _subtracted_value The amount of to decrease the allowance - @return bool success - """ - allowance: uint256 = self.allowance[msg.sender][_spender] - _subtracted_value - self.allowance[msg.sender][_spender] = allowance - - log Approval(msg.sender, _spender, allowance) - - return True - - -@external -def user_checkpoint(addr: address) -> bool: - """ - @notice Record a checkpoint for `addr` - @param addr User address - @return bool success - """ - assert msg.sender in [addr, MINTER] # dev: unauthorized - self._checkpoint(addr) - self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) - return True - - -@external -def set_rewards_receiver(_receiver: address): - """ - @notice Set the default reward receiver for the caller. - @dev When set to empty(address), rewards are sent to the caller - @param _receiver Receiver address for any rewards claimed via `claim_rewards` - """ - self.rewards_receiver[msg.sender] = _receiver - - -@external -def kick(addr: address): - """ - @notice Kick `addr` for abusing their boost - @dev Only if either they had another voting event, or their voting escrow lock expired - @param addr Address to kick - """ - t_last: uint256 = self.integrate_checkpoint_of[addr] - t_ve: uint256 = VotingEscrow(VOTING_ESCROW).user_point_history__ts( - addr, VotingEscrow(VOTING_ESCROW).user_point_epoch(addr) - ) - _balance: uint256 = self.balanceOf[addr] - - assert ERC20(VOTING_ESCROW).balanceOf(addr) == 0 or t_ve > t_last # dev: kick not allowed - assert self.working_balances[addr] > _balance * TOKENLESS_PRODUCTION / 100 # dev: kick not needed - - self._checkpoint(addr) - self._update_liquidity_limit(addr, self.balanceOf[addr], self.totalSupply) - - -# Administrative Functions - - -@external -def set_gauge_manager(_gauge_manager: address): - """ - @notice Change the gauge manager for a gauge - @dev The manager of this contract, or the ownership admin can outright modify gauge - managership. A gauge manager can also transfer managership to a new manager via this - method, but only for the gauge which they are the manager of. - @param _gauge_manager The account to set as the new manager of the gauge. - """ - assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin - - self.manager = _gauge_manager - log SetGaugeManager(_gauge_manager) - - -@external -@nonreentrant("lock") -def deposit_reward_token(_reward_token: address, _amount: uint256, _epoch: uint256 = WEEK): - """ - @notice Deposit a reward token for distribution - @param _reward_token The reward token being deposited - @param _amount The amount of `_reward_token` being deposited - @param _epoch The duration the rewards are distributed across. - """ - assert msg.sender == self.reward_data[_reward_token].distributor - - self._checkpoint_rewards(empty(address), self.totalSupply, False, empty(address)) - - # transferFrom reward token and use transferred amount henceforth: - amount_received: uint256 = ERC20(_reward_token).balanceOf(self) - assert ERC20(_reward_token).transferFrom( - msg.sender, - self, - _amount, - default_return_value=True - ) - amount_received = ERC20(_reward_token).balanceOf(self) - amount_received - - period_finish: uint256 = self.reward_data[_reward_token].period_finish - assert amount_received > _epoch # dev: rate will tend to zero! - - if block.timestamp >= period_finish: - self.reward_data[_reward_token].rate = amount_received / _epoch - else: - remaining: uint256 = period_finish - block.timestamp - leftover: uint256 = remaining * self.reward_data[_reward_token].rate - self.reward_data[_reward_token].rate = (amount_received + leftover) / _epoch - - self.reward_data[_reward_token].last_update = block.timestamp - self.reward_data[_reward_token].period_finish = block.timestamp + _epoch - - -@external -def add_reward(_reward_token: address, _distributor: address): - """ - @notice Add additional rewards to be distributed to stakers - @param _reward_token The token to add as an additional reward - @param _distributor Address permitted to fund this contract with the reward token - """ - assert msg.sender in [self.manager, Factory(self.factory).admin()] # dev: only manager or factory admin - assert _distributor != empty(address) # dev: distributor cannot be zero address - - reward_count: uint256 = self.reward_count - assert reward_count < MAX_REWARDS - assert self.reward_data[_reward_token].distributor == empty(address) - - self.reward_data[_reward_token].distributor = _distributor - self.reward_tokens[reward_count] = _reward_token - self.reward_count = reward_count + 1 - - -@external -def set_reward_distributor(_reward_token: address, _distributor: address): - """ - @notice Reassign the reward distributor for a reward token - @param _reward_token The reward token to reassign distribution rights to - @param _distributor The address of the new distributor - """ - current_distributor: address = self.reward_data[_reward_token].distributor - - assert msg.sender in [current_distributor, Factory(self.factory).admin(), self.manager] - assert current_distributor != empty(address) - assert _distributor != empty(address) - - self.reward_data[_reward_token].distributor = _distributor - - -@external -def set_killed(_is_killed: bool): - """ - @notice Set the killed status for this contract - @dev When killed, the gauge always yields a rate of 0 and so cannot mint CRV - @param _is_killed Killed status to set - """ - assert msg.sender == Factory(self.factory).admin() # dev: only owner - - self.is_killed = _is_killed - - -# View Methods - - -@view -@external -def claimed_reward(_addr: address, _token: address) -> uint256: - """ - @notice Get the number of already-claimed reward tokens for a user - @param _addr Account to get reward amount for - @param _token Token to get reward amount for - @return uint256 Total amount of `_token` already claimed by `_addr` - """ - return self.claim_data[_addr][_token] % 2**128 - - -@view -@external -def claimable_reward(_user: address, _reward_token: address) -> uint256: - """ - @notice Get the number of claimable reward tokens for a user - @param _user Account to get reward amount for - @param _reward_token Token to get reward amount for - @return uint256 Claimable reward token amount - """ - integral: uint256 = self.reward_data[_reward_token].integral - total_supply: uint256 = self.totalSupply - if total_supply != 0: - last_update: uint256 = min(block.timestamp, self.reward_data[_reward_token].period_finish) - duration: uint256 = last_update - self.reward_data[_reward_token].last_update - integral += (duration * self.reward_data[_reward_token].rate * 10**18 / total_supply) - - integral_for: uint256 = self.reward_integral_for[_reward_token][_user] - new_claimable: uint256 = self.balanceOf[_user] * (integral - integral_for) / 10**18 - - return (self.claim_data[_user][_reward_token] >> 128) + new_claimable - - -@external -def claimable_tokens(addr: address) -> uint256: - """ - @notice Get the number of claimable tokens per user - @dev This function should be manually changed to "view" in the ABI - @return uint256 number of claimable tokens per user - """ - self._checkpoint(addr) - return self.integrate_fraction[addr] - Minter(MINTER).minted(addr, self) - - -@view -@external -def integrate_checkpoint() -> uint256: - """ - @notice Get the timestamp of the last checkpoint - """ - return self.period_timestamp[self.period] - - -@view -@external -def future_epoch_time() -> uint256: - """ - @notice Get the locally stored CRV future epoch start time - """ - return self.inflation_params >> 216 - - -@view -@external -def inflation_rate() -> uint256: - """ - @notice Get the locally stored CRV inflation rate - """ - return self.inflation_params % 2 ** 216 - - -@view -@external -def decimals() -> uint256: - """ - @notice Get the number of decimals for this token - @dev Implemented as a view method to reduce gas costs - @return uint256 decimal places - """ - return 18 - - -@view -@external -def version() -> String[8]: - """ - @notice Get the version of this gauge contract - """ - return VERSION - - -@view -@external -def DOMAIN_SEPARATOR() -> bytes32: - """ - @notice EIP712 domain separator. - """ - return self._domain_separator() diff --git a/contracts/lending/OneWayLendingFactory.vy b/contracts/lending/OneWayLendingFactory.vy deleted file mode 100644 index 78634fcb..00000000 --- a/contracts/lending/OneWayLendingFactory.vy +++ /dev/null @@ -1,435 +0,0 @@ -# @version 0.3.10 -""" -@title OneWayLendingFactory -@notice Factory of non-rehypothecated lending vaults: collateral is not being lent out. - Although Vault.vy allows both, we should have this simpler version and rehypothecating version. -@author Curve.fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" - -interface Vault: - def initialize( - amm_impl: address, - controller_impl: address, - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - price_oracle: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256 - ) -> (address, address): nonpayable - def amm() -> address: view - def controller() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view - def price_oracle() -> address: view - def set_max_supply(_value: uint256): nonpayable - -interface Controller: - def monetary_policy() -> address: view - -interface AMM: - def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view - def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view - def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): view - def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address) -> uint256[2]: nonpayable - def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address) -> uint256[2]: nonpayable - -interface Pool: - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - def coins(i: uint256) -> address: view - - -event SetImplementations: - amm: address - controller: address - vault: address - price_oracle: address - monetary_policy: address - gauge: address - -event SetDefaultRates: - min_rate: uint256 - max_rate: uint256 - -event SetAdmin: - admin: address - -event NewVault: - id: indexed(uint256) - collateral_token: indexed(address) - borrowed_token: indexed(address) - vault: address - controller: address - amm: address - price_oracle: address - monetary_policy: address - -event LiquidityGaugeDeployed: - vault: address - gauge: address - - -STABLECOIN: public(immutable(address)) - -# These are limits for default borrow rates, NOT actual min and max rates. -# Even governance cannot go beyond these rates before a new code is shipped -MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400) # 0.1% -MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400) # 1000% - - -# Implementations which can be changed by governance -amm_impl: public(address) -controller_impl: public(address) -vault_impl: public(address) -pool_price_oracle_impl: public(address) -monetary_policy_impl: public(address) -gauge_impl: public(address) - -# Actual min/max borrow rates when creating new markets -# for example, 0.5% -> 50% is a good choice -min_default_borrow_rate: public(uint256) -max_default_borrow_rate: public(uint256) - -# Admin is supposed to be the DAO -admin: public(address) - -# Vaults can only be created but not removed -vaults: public(Vault[10**18]) -amms: public(AMM[10**18]) -_vaults_index: HashMap[Vault, uint256] -market_count: public(uint256) - -# Index to find vaults by a non-crvUSD token -token_to_vaults: public(HashMap[address, Vault[10**18]]) -token_market_count: public(HashMap[address, uint256]) - -gauges: public(address[10**18]) -names: public(HashMap[uint256, String[64]]) - - -@external -def __init__( - stablecoin: address, - amm: address, - controller: address, - vault: address, - pool_price_oracle: address, - monetary_policy: address, - gauge: address, - admin: address): - """ - @notice Factory which creates one-way lending vaults (e.g. collateral is non-borrowable) - @param stablecoin Address of crvUSD. Only crvUSD-containing markets are allowed - @param amm Address of AMM implementation - @param controller Address of Controller implementation - @param pool_price_oracle Address of implementation for price oracle factory (prices from pools) - @param monetary_policy Address for implementation of monetary policy - @param gauge Address for gauge implementation - @param admin Admin address (DAO) - """ - STABLECOIN = stablecoin - self.amm_impl = amm - self.controller_impl = controller - self.vault_impl = vault - self.pool_price_oracle_impl = pool_price_oracle - self.monetary_policy_impl = monetary_policy - self.gauge_impl = gauge - - self.min_default_borrow_rate = 5 * 10**15 / (365 * 86400) - self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) - - self.admin = admin - - -@internal -def _create( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - price_oracle: address, - name: String[64], - min_borrow_rate: uint256, - max_borrow_rate: uint256 - ) -> Vault: - """ - @notice Internal method for creation of the vault - """ - assert borrowed_token != collateral_token, "Same token" - assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN - vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - - min_rate: uint256 = self.min_default_borrow_rate - max_rate: uint256 = self.max_default_borrow_rate - if min_borrow_rate > 0: - min_rate = min_borrow_rate - if max_borrow_rate > 0: - max_rate = max_borrow_rate - assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates" - monetary_policy: address = create_from_blueprint( - self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) - - controller: address = empty(address) - amm: address = empty(address) - controller, amm = vault.initialize( - self.amm_impl, self.controller_impl, - borrowed_token, collateral_token, - A, fee, - price_oracle, - monetary_policy, - loan_discount, liquidation_discount - ) - - market_count: uint256 = self.market_count - log NewVault(market_count, collateral_token, borrowed_token, vault.address, controller, amm, price_oracle, monetary_policy) - self.vaults[market_count] = vault - self.amms[market_count] = AMM(amm) - self._vaults_index[vault] = market_count + 2**128 - self.names[market_count] = name - - self.market_count = market_count + 1 - - token: address = borrowed_token - if borrowed_token == STABLECOIN: - token = collateral_token - market_count = self.token_market_count[token] - self.token_to_vaults[token][market_count] = vault - self.token_market_count[token] = market_count + 1 - - return vault - - -@external -@nonreentrant('lock') -def create( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - price_oracle: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0, - supply_limit: uint256 = max_value(uint256) - ) -> Vault: - """ - @notice Creation of the vault using user-supplied price oracle contract - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param price_oracle Custom price oracle contract - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - @param supply_limit Supply cap - """ - vault: Vault = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate) - if supply_limit < max_value(uint256): - vault.set_max_supply(supply_limit) - return vault - - -@external -@nonreentrant('lock') -def create_from_pool( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - pool: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0, - supply_limit: uint256 = max_value(uint256) - ) -> Vault: - """ - @notice Creation of the vault using existing oraclized Curve pool as a price oracle - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param pool Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable price_oracle(). - Must contain both collateral_token and borrowed_token. - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - @param supply_limit Supply cap - """ - # Find coins in the pool - borrowed_ix: uint256 = 100 - collateral_ix: uint256 = 100 - N: uint256 = 0 - for i in range(10): - success: bool = False - res: Bytes[32] = empty(Bytes[32]) - success, res = raw_call( - pool, - _abi_encode(i, method_id=method_id("coins(uint256)")), - max_outsize=32, is_static_call=True, revert_on_failure=False) - coin: address = convert(res, address) - if not success or coin == empty(address): - break - N += 1 - if coin == borrowed_token: - borrowed_ix = i - elif coin == collateral_token: - collateral_ix = i - if collateral_ix == 100 or borrowed_ix == 100: - raise "Tokens not in pool" - price_oracle: address = create_from_blueprint( - self.pool_price_oracle_impl, pool, N, borrowed_ix, collateral_ix, code_offset=3) - - vault: Vault = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate) - if supply_limit < max_value(uint256): - vault.set_max_supply(supply_limit) - return vault - - -@view -@external -def controllers(n: uint256) -> address: - return self.vaults[n].controller() - - -@view -@external -def borrowed_tokens(n: uint256) -> address: - return self.vaults[n].borrowed_token() - - -@view -@external -def collateral_tokens(n: uint256) -> address: - return self.vaults[n].collateral_token() - - -@view -@external -def price_oracles(n: uint256) -> address: - return self.vaults[n].price_oracle() - - -@view -@external -def monetary_policies(n: uint256) -> address: - return Controller(self.vaults[n].controller()).monetary_policy() - - -@view -@external -def vaults_index(vault: Vault) -> uint256: - return self._vaults_index[vault] - 2**128 - - -@external -def deploy_gauge(_vault: Vault) -> address: - """ - @notice Deploy a liquidity gauge for a vault - @param _vault Vault address to deploy a gauge for - @return Address of the deployed gauge - """ - ix: uint256 = self._vaults_index[_vault] - assert ix != 0, "Unknown vault" - ix -= 2**128 - assert self.gauges[ix] == empty(address), "Gauge already deployed" - implementation: address = self.gauge_impl - assert implementation != empty(address), "Gauge implementation not set" - - gauge: address = create_from_blueprint(implementation, _vault, code_offset=3) - self.gauges[ix] = gauge - - log LiquidityGaugeDeployed(_vault.address, gauge) - return gauge - - -@view -@external -def gauge_for_vault(_vault: Vault) -> address: - return self.gauges[self._vaults_index[_vault] - 2**128] - - -@external -@nonreentrant('lock') -def set_implementations(controller: address, amm: address, vault: address, - pool_price_oracle: address, monetary_policy: address, gauge: address): - """ - @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy. - Doesn't change existing ones - @param controller Address of the controller blueprint - @param amm Address of the AMM blueprint - @param vault Address of the Vault template - @param pool_price_oracle Address of the pool price oracle blueprint - @param monetary_policy Address of the monetary policy blueprint - @param gauge Address for gauge implementation blueprint - """ - assert msg.sender == self.admin - - if controller != empty(address): - self.controller_impl = controller - if amm != empty(address): - self.amm_impl = amm - if vault != empty(address): - self.vault_impl = vault - if pool_price_oracle != empty(address): - self.pool_price_oracle_impl = pool_price_oracle - if monetary_policy != empty(address): - self.monetary_policy_impl = monetary_policy - if gauge != empty(address): - self.gauge_impl = gauge - - log SetImplementations(amm, controller, vault, pool_price_oracle, monetary_policy, gauge) - - -@external -@nonreentrant('lock') -def set_default_rates(min_rate: uint256, max_rate: uint256): - """ - @notice Change min and max default borrow rates for creating new markets - @param min_rate Minimal borrow rate (0 utilization) - @param max_rate Maxumum borrow rate (100% utilization) - """ - assert msg.sender == self.admin - - assert min_rate >= MIN_RATE - assert max_rate <= MAX_RATE - assert max_rate >= min_rate - - self.min_default_borrow_rate = min_rate - self.max_default_borrow_rate = max_rate - - log SetDefaultRates(min_rate, max_rate) - - -@external -@nonreentrant('lock') -def set_admin(admin: address): - """ - @notice Set admin of the factory (should end up with DAO) - @param admin Address of the admin - """ - assert msg.sender == self.admin - self.admin = admin - log SetAdmin(admin) - - -@external -@view -def coins(vault_id: uint256) -> address[2]: - vault: Vault = self.vaults[vault_id] - return [vault.borrowed_token(), vault.collateral_token()] diff --git a/contracts/lending/OneWayLendingFactoryL2.vy b/contracts/lending/OneWayLendingFactoryL2.vy deleted file mode 100644 index d2204626..00000000 --- a/contracts/lending/OneWayLendingFactoryL2.vy +++ /dev/null @@ -1,421 +0,0 @@ -# @version 0.3.10 -""" -@title OneWayLendingFactory -@notice Factory of non-rehypothecated lending vaults: collateral is not being lent out. - Although Vault.vy allows both, we should have this simpler version and rehypothecating version. - This version is for L2s: it does not create gauges by itself but uses Gauge Factory to read gauge info. -@author Curve.fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" - -interface Vault: - def initialize( - amm_impl: address, - controller_impl: address, - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - price_oracle: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256 - ) -> (address, address): nonpayable - def amm() -> address: view - def controller() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view - def price_oracle() -> address: view - def set_max_supply(_value: uint256): nonpayable - -interface Controller: - def monetary_policy() -> address: view - -interface AMM: - def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view - def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view - def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): view - def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address) -> uint256[2]: nonpayable - def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address) -> uint256[2]: nonpayable - -interface Pool: - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - def coins(i: uint256) -> address: view - -interface GaugeFactory: - def get_gauge_from_lp_token(addr: address) -> address: view - - -event SetImplementations: - amm: address - controller: address - vault: address - price_oracle: address - monetary_policy: address - gauge_factory: address - -event SetDefaultRates: - min_rate: uint256 - max_rate: uint256 - -event SetAdmin: - admin: address - -event NewVault: - id: indexed(uint256) - collateral_token: indexed(address) - borrowed_token: indexed(address) - vault: address - controller: address - amm: address - price_oracle: address - monetary_policy: address - - -STABLECOIN: public(immutable(address)) - -# These are limits for default borrow rates, NOT actual min and max rates. -# Even governance cannot go beyond these rates before a new code is shipped -MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400) # 0.1% -MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400) # 1000% - - -# Implementations which can be changed by governance -amm_impl: public(address) -controller_impl: public(address) -vault_impl: public(address) -pool_price_oracle_impl: public(address) -monetary_policy_impl: public(address) - -# Actual min/max borrow rates when creating new markets -# for example, 0.5% -> 50% is a good choice -min_default_borrow_rate: public(uint256) -max_default_borrow_rate: public(uint256) - -# Admin is supposed to be the DAO -admin: public(address) - -# Vaults can only be created but not removed -vaults: public(Vault[10**18]) -amms: public(AMM[10**18]) -_vaults_index: HashMap[Vault, uint256] -market_count: public(uint256) - -# Index to find vaults by a non-crvUSD token -token_to_vaults: public(HashMap[address, Vault[10**18]]) -token_market_count: public(HashMap[address, uint256]) - -names: public(HashMap[uint256, String[64]]) -gauge_factory: public(GaugeFactory) - - -@external -def __init__( - stablecoin: address, - amm: address, - controller: address, - vault: address, - pool_price_oracle: address, - monetary_policy: address, - gauge_factory: GaugeFactory, - admin: address): - """ - @notice Factory which creates one-way lending vaults (e.g. collateral is non-borrowable) - @param stablecoin Address of crvUSD. Only crvUSD-containing markets are allowed - @param amm Address of AMM implementation - @param controller Address of Controller implementation - @param pool_price_oracle Address of implementation for price oracle factory (prices from pools) - @param monetary_policy Address for implementation of monetary policy - @param gauge_factory Address for gauge factory on this L2 - @param admin Admin address (DAO) - """ - STABLECOIN = stablecoin - self.amm_impl = amm - self.controller_impl = controller - self.vault_impl = vault - self.pool_price_oracle_impl = pool_price_oracle - self.monetary_policy_impl = monetary_policy - self.gauge_factory = gauge_factory - - self.min_default_borrow_rate = 5 * 10**15 / (365 * 86400) - self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) - - self.admin = admin - - -@internal -def _create( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - price_oracle: address, - name: String[64], - min_borrow_rate: uint256, - max_borrow_rate: uint256 - ) -> Vault: - """ - @notice Internal method for creation of the vault - """ - assert borrowed_token != collateral_token, "Same token" - assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN - vault: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - - min_rate: uint256 = self.min_default_borrow_rate - max_rate: uint256 = self.max_default_borrow_rate - if min_borrow_rate > 0: - min_rate = min_borrow_rate - if max_borrow_rate > 0: - max_rate = max_borrow_rate - assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates" - monetary_policy: address = create_from_blueprint( - self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) - - controller: address = empty(address) - amm: address = empty(address) - controller, amm = vault.initialize( - self.amm_impl, self.controller_impl, - borrowed_token, collateral_token, - A, fee, - price_oracle, - monetary_policy, - loan_discount, liquidation_discount - ) - - market_count: uint256 = self.market_count - log NewVault(market_count, collateral_token, borrowed_token, vault.address, controller, amm, price_oracle, monetary_policy) - self.vaults[market_count] = vault - self.amms[market_count] = AMM(amm) - self._vaults_index[vault] = market_count + 2**128 - self.names[market_count] = name - - self.market_count = market_count + 1 - - token: address = borrowed_token - if borrowed_token == STABLECOIN: - token = collateral_token - market_count = self.token_market_count[token] - self.token_to_vaults[token][market_count] = vault - self.token_market_count[token] = market_count + 1 - - return vault - - -@external -@nonreentrant('lock') -def create( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - price_oracle: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0, - supply_limit: uint256 = max_value(uint256) - ) -> Vault: - """ - @notice Creation of the vault using user-supplied price oracle contract - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param price_oracle Custom price oracle contract - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - @param supply_limit Supply cap - """ - vault: Vault = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate) - if supply_limit < max_value(uint256): - vault.set_max_supply(supply_limit) - return vault - - -@external -@nonreentrant('lock') -def create_from_pool( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - pool: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0, - supply_limit: uint256 = max_value(uint256) - ) -> Vault: - """ - @notice Creation of the vault using existing oraclized Curve pool as a price oracle - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param pool Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable price_oracle(). - Must contain both collateral_token and borrowed_token. - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - @param supply_limit Supply cap - """ - # Find coins in the pool - borrowed_ix: uint256 = 100 - collateral_ix: uint256 = 100 - N: uint256 = 0 - for i in range(10): - success: bool = False - res: Bytes[32] = empty(Bytes[32]) - success, res = raw_call( - pool, - _abi_encode(i, method_id=method_id("coins(uint256)")), - max_outsize=32, is_static_call=True, revert_on_failure=False) - coin: address = convert(res, address) - if not success or coin == empty(address): - break - N += 1 - if coin == borrowed_token: - borrowed_ix = i - elif coin == collateral_token: - collateral_ix = i - if collateral_ix == 100 or borrowed_ix == 100: - raise "Tokens not in pool" - price_oracle: address = create_from_blueprint( - self.pool_price_oracle_impl, pool, N, borrowed_ix, collateral_ix, code_offset=3) - - vault: Vault = self._create(borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, - price_oracle, name, min_borrow_rate, max_borrow_rate) - if supply_limit < max_value(uint256): - vault.set_max_supply(supply_limit) - return vault - - -@view -@external -def controllers(n: uint256) -> address: - return self.vaults[n].controller() - - -@view -@external -def borrowed_tokens(n: uint256) -> address: - return self.vaults[n].borrowed_token() - - -@view -@external -def collateral_tokens(n: uint256) -> address: - return self.vaults[n].collateral_token() - - -@view -@external -def price_oracles(n: uint256) -> address: - return self.vaults[n].price_oracle() - - -@view -@external -def monetary_policies(n: uint256) -> address: - return Controller(self.vaults[n].controller()).monetary_policy() - - -@view -@external -def vaults_index(vault: Vault) -> uint256: - return self._vaults_index[vault] - 2**128 - - -@view -@external -def gauge_for_vault(vault: address) -> address: - out: address = self.gauge_factory.get_gauge_from_lp_token(vault) - assert out != empty(address) - return out - - -@view -@external -def gauges(vault_id: uint256) -> address: - return self.gauge_factory.get_gauge_from_lp_token(self.vaults[vault_id].address) - - -@external -@nonreentrant('lock') -def set_implementations(controller: address, amm: address, vault: address, - pool_price_oracle: address, monetary_policy: address, gauge_factory: address): - """ - @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy. - Doesn't change existing ones - @param controller Address of the controller blueprint - @param amm Address of the AMM blueprint - @param vault Address of the Vault template - @param pool_price_oracle Address of the pool price oracle blueprint - @param monetary_policy Address of the monetary policy blueprint - @param gauge_factory Address for gauge factory - """ - assert msg.sender == self.admin - - if controller != empty(address): - self.controller_impl = controller - if amm != empty(address): - self.amm_impl = amm - if vault != empty(address): - self.vault_impl = vault - if pool_price_oracle != empty(address): - self.pool_price_oracle_impl = pool_price_oracle - if monetary_policy != empty(address): - self.monetary_policy_impl = monetary_policy - if gauge_factory != empty(address): - self.gauge_factory = GaugeFactory(gauge_factory) - - log SetImplementations(amm, controller, vault, pool_price_oracle, monetary_policy, gauge_factory) - - -@external -@nonreentrant('lock') -def set_default_rates(min_rate: uint256, max_rate: uint256): - """ - @notice Change min and max default borrow rates for creating new markets - @param min_rate Minimal borrow rate (0 utilization) - @param max_rate Maxumum borrow rate (100% utilization) - """ - assert msg.sender == self.admin - - assert min_rate >= MIN_RATE - assert max_rate <= MAX_RATE - assert max_rate >= min_rate - - self.min_default_borrow_rate = min_rate - self.max_default_borrow_rate = max_rate - - log SetDefaultRates(min_rate, max_rate) - - -@external -@nonreentrant('lock') -def set_admin(admin: address): - """ - @notice Set admin of the factory (should end up with DAO) - @param admin Address of the admin - """ - assert msg.sender == self.admin - self.admin = admin - log SetAdmin(admin) - - -@external -@view -def coins(vault_id: uint256) -> address[2]: - vault: Vault = self.vaults[vault_id] - return [vault.borrowed_token(), vault.collateral_token()] diff --git a/contracts/lending/TwoWayLendingFactory.vy b/contracts/lending/TwoWayLendingFactory.vy deleted file mode 100644 index 50781d0f..00000000 --- a/contracts/lending/TwoWayLendingFactory.vy +++ /dev/null @@ -1,625 +0,0 @@ -# @version 0.3.10 -""" -@title TwoWayLendingFactory -@notice Factory of rehypothecated lending vaults: collateral can be lent out. -@author Curve.fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved -""" -from vyper.interfaces import ERC20 - -interface Vault: - def initialize( - amm_impl: address, - controller_impl: address, - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - price_oracle: address, - monetary_policy: address, - loan_discount: uint256, - liquidation_discount: uint256 - ) -> (address, address): nonpayable - def amm() -> address: view - def controller() -> address: view - def borrowed_token() -> address: view - def collateral_token() -> address: view - def price_oracle() -> address: view - def convertToShares(assets: uint256) -> uint256: view - def convertToAssets(shares: uint256) -> uint256: view - def deposit(assets: uint256) -> uint256: nonpayable - def redeem(shares: uint256, receiver: address) -> uint256: nonpayable - def balanceOf(user: address) -> uint256: view - -interface Controller: - def monetary_policy() -> address: view - -interface AMM: - def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view - def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: view - def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): view - def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address) -> uint256[2]: nonpayable - def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address) -> uint256[2]: nonpayable - -interface Pool: - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - - -event SetImplementations: - amm: address - controller: address - vault: address - pool_price_oracle: address - wrapper_price_oracle: address - monetary_policy: address - gauge: address - -event SetDefaultRates: - min_rate: uint256 - max_rate: uint256 - -event SetAdmin: - admin: address - -# TWO events are emitted at market creation because there are two vaults -event NewVault: - id: indexed(uint256) - collateral_token: indexed(address) - borrowed_token: indexed(address) - vault: address - controller: address - amm: address - price_oracle: address - monetary_policy: address - -event LiquidityGaugeDeployed: - vault: address - gauge: address - - -STABLECOIN: public(immutable(address)) - -# These are limits for default borrow rates, NOT actual min and max rates. -# Even governance cannot go beyond these rates before a new code is shipped -MIN_RATE: public(constant(uint256)) = 10**15 / (365 * 86400) # 0.1% -MAX_RATE: public(constant(uint256)) = 10**19 / (365 * 86400) # 1000% - - -# Implementations which can be changed by governance -amm_impl: public(address) -controller_impl: public(address) -vault_impl: public(address) -pool_price_oracle_impl: public(address) -wrapper_price_oracle_impl: public(address) -monetary_policy_impl: public(address) -gauge_impl: public(address) - -# Actual min/max borrow rates when creating new markets -# for example, 0.5% -> 50% is a good choice -min_default_borrow_rate: public(uint256) -max_default_borrow_rate: public(uint256) - -# Admin is supposed to be the DAO -admin: public(address) - -# Vaults can only be created but not removed -vaults: public(Vault[10**18]) -amms: public(AMM[10**18]) -_vaults_index: HashMap[Vault, uint256] -market_count: public(uint256) - -# Index to find vaults by a non-crvUSD token -token_to_vaults: public(HashMap[address, Vault[10**18]]) -token_market_count: public(HashMap[address, uint256]) - -gauges: public(address[10**18]) -names: public(HashMap[uint256, String[71]]) - - -@external -def __init__( - stablecoin: address, - amm: address, - controller: address, - vault: address, - pool_price_oracle: address, - wrapper_price_oracle: address, - monetary_policy: address, - gauge: address, - admin: address): - """ - @notice Factory which creates two-way lending vaults (e.g. collateral is borrowable) - @param stablecoin Address of crvUSD. Only crvUSD-containing markets are allowed - @param amm Address of AMM implementation - @param controller Address of Controller implementation - @param pool_price_oracle Address of implementation for pool price oracle factory (prices from pools) - @param wrapper_price_oracle Address of implementation for price oracle wrapper factory (multiplies by vault pricePerShare) - @param monetary_policy Address for implementation of monetary policy - @param gauge Address for gauge implementation - @param admin Admin address (DAO) - """ - STABLECOIN = stablecoin - self.amm_impl = amm - self.controller_impl = controller - self.vault_impl = vault - self.pool_price_oracle_impl = pool_price_oracle - self.wrapper_price_oracle_impl = wrapper_price_oracle - self.monetary_policy_impl = monetary_policy - self.gauge_impl = gauge - - self.min_default_borrow_rate = 5 * 10**15 / (365 * 86400) - self.max_default_borrow_rate = 50 * 10**16 / (365 * 86400) - - self.admin = admin - - -@internal -def _add_to_index(vault: Vault, token_a: address, token_b: address): - token: address = token_a - if token_a == STABLECOIN: - token = token_b - market_count: uint256 = self.token_market_count[token] - self.token_to_vaults[token][market_count] = vault - self.token_market_count[token] = market_count + 1 - - -@internal -def _create( - vault_long: Vault, - vault_short: Vault, - price_oracle_long: address, - price_oracle_short: address, - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - name: String[64], - min_borrow_rate: uint256, - max_borrow_rate: uint256): - """ - @notice Internal method for creation of the vault - """ - assert borrowed_token != collateral_token, "Same token" - assert borrowed_token == STABLECOIN or collateral_token == STABLECOIN - - min_rate: uint256 = self.min_default_borrow_rate - max_rate: uint256 = self.max_default_borrow_rate - if min_borrow_rate > 0: - min_rate = min_borrow_rate - if max_borrow_rate > 0: - max_rate = max_borrow_rate - assert min_rate >= MIN_RATE and max_rate <= MAX_RATE\ - and min_rate <= max_rate, "Wrong rates" - monetary_policy_long: address = create_from_blueprint( - self.monetary_policy_impl, borrowed_token, min_rate, max_rate, code_offset=3) - monetary_policy_short: address = create_from_blueprint( - self.monetary_policy_impl, collateral_token, min_rate, max_rate, code_offset=3) - - controller: address = empty(address) - amm: address = empty(address) - market_count: uint256 = self.market_count - - controller, amm = vault_long.initialize( - self.amm_impl, self.controller_impl, - borrowed_token, vault_short.address, - A, fee, - price_oracle_long, - monetary_policy_long, - loan_discount, liquidation_discount - ) - - log NewVault(market_count, vault_short.address, borrowed_token, vault_long.address, controller, amm, price_oracle_long, monetary_policy_long) - self.vaults[market_count] = vault_long - self.amms[market_count] = AMM(amm) - self.names[market_count] = concat(name, ": long") - self._vaults_index[vault_long] = market_count + 2**128 - market_count += 1 - - assert ERC20(borrowed_token).approve(amm, max_value(uint256), default_return_value=True) - assert ERC20(collateral_token).approve(vault_short.address, max_value(uint256), default_return_value=True) - assert ERC20(vault_short.address).approve(amm, max_value(uint256), default_return_value=True) - - controller, amm = vault_short.initialize( - self.amm_impl, self.controller_impl, - collateral_token, vault_long.address, - A, fee, - price_oracle_short, - monetary_policy_short, - loan_discount, liquidation_discount - ) - log NewVault(market_count, vault_long.address, collateral_token, vault_short.address, controller, amm, price_oracle_short, monetary_policy_short) - self.vaults[market_count] = vault_short - self.amms[market_count] = AMM(amm) - self.names[market_count] = concat(name, ": short") - self._vaults_index[vault_short] = market_count + 2**128 - market_count += 1 - self.market_count = market_count - - assert ERC20(borrowed_token).approve(vault_long.address, max_value(uint256), default_return_value=True) - assert ERC20(collateral_token).approve(amm, max_value(uint256), default_return_value=True) - assert ERC20(vault_long.address).approve(amm, max_value(uint256), default_return_value=True) - - self._add_to_index(vault_long, borrowed_token, collateral_token) - self._add_to_index(vault_short, borrowed_token, collateral_token) - - -@external -@nonreentrant('lock') -def create( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - price_oracle: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0 - ) -> (Vault, Vault): - """ - @notice Creation of the vault using user-supplied price oracle contract - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param price_oracle Custom price oracle contract - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - """ - vault_long: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - vault_short: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - - price_oracle_long: address = create_from_blueprint( - self.wrapper_price_oracle_impl, price_oracle, vault_short.address, False, code_offset=3) - price_oracle_short: address = create_from_blueprint( - self.wrapper_price_oracle_impl, price_oracle, vault_long.address, True, code_offset=3) - - self._create(vault_long, vault_short, price_oracle_long, price_oracle_short, - borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, name, - min_borrow_rate, max_borrow_rate) - - return (vault_long, vault_short) - - -@external -@nonreentrant('lock') -def create_from_pool( - borrowed_token: address, - collateral_token: address, - A: uint256, - fee: uint256, - loan_discount: uint256, - liquidation_discount: uint256, - pool: address, - name: String[64], - min_borrow_rate: uint256 = 0, - max_borrow_rate: uint256 = 0 - ) -> (Vault, Vault): - """ - @notice Creation of the vault using existing oraclized Curve pool as a price oracle - @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount - @param pool Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable price_oracle(). - Must contain both collateral_token and borrowed_token. - @param name Human-readable market name - @param min_borrow_rate Custom minimum borrow rate (otherwise min_default_borrow_rate) - @param max_borrow_rate Custom maximum borrow rate (otherwise max_default_borrow_rate) - """ - # Find coins in the pool - borrowed_ix: uint256 = 100 - collateral_ix: uint256 = 100 - N: uint256 = 0 - for i in range(10): - success: bool = False - res: Bytes[32] = empty(Bytes[32]) - success, res = raw_call( - pool, - _abi_encode(i, method_id=method_id("coins(uint256)")), - max_outsize=32, is_static_call=True, revert_on_failure=False) - coin: address = convert(res, address) - if not success or coin == empty(address): - break - N += 1 - if coin == borrowed_token: - borrowed_ix = i - elif coin == collateral_token: - collateral_ix = i - if collateral_ix == 100 or borrowed_ix == 100: - raise "Tokens not in pool" - - vault_long: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - vault_short: Vault = Vault(create_minimal_proxy_to(self.vault_impl)) - - price_oracle_long: address = create_from_blueprint( - self.pool_price_oracle_impl, pool, N, borrowed_ix, collateral_ix, vault_short.address, code_offset=3) - price_oracle_short: address = create_from_blueprint( - self.pool_price_oracle_impl, pool, N, collateral_ix, borrowed_ix, vault_long.address, code_offset=3) - - self._create(vault_long, vault_short, price_oracle_long, price_oracle_short, - borrowed_token, collateral_token, A, fee, loan_discount, liquidation_discount, name, - min_borrow_rate, max_borrow_rate) - - return (vault_long, vault_short) - - -@view -@external -def controllers(n: uint256) -> address: - return self.vaults[n].controller() - - -@view -@external -def borrowed_tokens(n: uint256) -> address: - return self.vaults[n].borrowed_token() - - -@view -@external -def collateral_tokens(n: uint256) -> address: - return self.vaults[n].collateral_token() - - -@view -@external -def price_oracles(n: uint256) -> address: - return self.vaults[n].price_oracle() - - -@view -@external -def monetary_policies(n: uint256) -> address: - return Controller(self.vaults[n].controller()).monetary_policy() - - -@view -@external -def vaults_index(vault: Vault) -> uint256: - return self._vaults_index[vault] - 2**128 - - -@external -def deploy_gauge(_vault: Vault) -> address: - """ - @notice Deploy a liquidity gauge for a vault - @param _vault Vault address to deploy a gauge for - @return Address of the deployed gauge - """ - ix: uint256 = self._vaults_index[_vault] - assert ix != 0, "Unknown vault" - ix -= 2**128 - assert self.gauges[ix] == empty(address), "Gauge already deployed" - implementation: address = self.gauge_impl - assert implementation != empty(address), "Gauge implementation not set" - - gauge: address = create_from_blueprint(implementation, _vault, code_offset=3) - self.gauges[ix] = gauge - - log LiquidityGaugeDeployed(_vault.address, gauge) - return gauge - - -@view -@external -def gauge_for_vault(_vault: Vault) -> address: - return self.gauges[self._vaults_index[_vault] - 2**128] - - -@external -@nonreentrant('lock') -def set_implementations(controller: address, amm: address, vault: address, - pool_price_oracle: address, wrapper_price_oracle: address, monetary_policy: address, - gauge: address): - """ - @notice Set new implementations (blueprints) for controller, amm, vault, pool price oracle and monetary polcy. - Doesn't change existing ones - @param controller Address of the controller blueprint - @param amm Address of the AMM blueprint - @param vault Address of the Vault template - @param pool_price_oracle Address of the pool price oracle blueprint - @param wrapper_price_oracle Address of the wrapper price oracle blueprint - @param monetary_policy Address of the monetary policy blueprint - @param gauge Address for gauge implementation blueprint - """ - assert msg.sender == self.admin - - if controller != empty(address): - self.controller_impl = controller - if amm != empty(address): - self.amm_impl = amm - if vault != empty(address): - self.vault_impl = vault - if pool_price_oracle != empty(address): - self.pool_price_oracle_impl = pool_price_oracle - if wrapper_price_oracle != empty(address): - self.wrapper_price_oracle_impl = wrapper_price_oracle - if monetary_policy != empty(address): - self.monetary_policy_impl = monetary_policy - if gauge != empty(address): - self.gauge_impl = gauge - - log SetImplementations(amm, controller, vault, pool_price_oracle, wrapper_price_oracle, monetary_policy, gauge) - - -@external -@nonreentrant('lock') -def set_default_rates(min_rate: uint256, max_rate: uint256): - """ - @notice Change min and max default borrow rates for creating new markets - @param min_rate Minimal borrow rate (0 utilization) - @param max_rate Maxumum borrow rate (100% utilization) - """ - assert msg.sender == self.admin - - assert min_rate >= MIN_RATE - assert max_rate <= MAX_RATE - assert max_rate >= min_rate - - self.min_default_borrow_rate = min_rate - self.max_default_borrow_rate = max_rate - - log SetDefaultRates(min_rate, max_rate) - - -@external -@nonreentrant('lock') -def set_admin(admin: address): - """ - @notice Set admin of the factory (should end up with DAO) - @param admin Address of the admin - """ - assert msg.sender == self.admin - self.admin = admin - log SetAdmin(admin) - - -@internal -@view -def other_vault(vault_id: uint256) -> Vault: - if vault_id % 2 == 0: - return self.vaults[vault_id + 1] - else: - return self.vaults[vault_id - 1] - - -@external -@view -def coins(vault_id: uint256) -> address[2]: - return [self.vaults[vault_id].borrowed_token(), self.other_vault(vault_id).borrowed_token()] - - -@external -@view -def get_dy(vault_id: uint256, i: uint256, j: uint256, amount: uint256) -> uint256: - dx: uint256 = amount - other_vault: Vault = self.other_vault(vault_id) - if i == 1: - dx = other_vault.convertToShares(amount) - dy: uint256 = self.amms[vault_id].get_dy(i, j, dx) - if j == 1: - dy = other_vault.convertToAssets(dy) - return dy - - -@external -@view -def get_dx(vault_id: uint256, i: uint256, j: uint256, out_amount: uint256) -> uint256: - dy: uint256 = out_amount - other_vault: Vault = self.other_vault(vault_id) - if j == 1: - dy = other_vault.convertToShares(out_amount) - dx: uint256 = self.amms[vault_id].get_dx(i, j, dy) - if i == 1: - dx = other_vault.convertToAssets(dx) - return dx - - -@external -@view -def get_dydx(vault_id: uint256, i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): - dy: uint256 = out_amount - other_vault: Vault = self.other_vault(vault_id) - if j == 1: - dy = other_vault.convertToShares(out_amount) - dx: uint256 = 0 - dy, dx = self.amms[vault_id].get_dydx(i, j, dy) - if j == 1: - dy = other_vault.convertToAssets(dy) - if i == 1: - dx = other_vault.convertToAssets(dx) - return dy, dx - - -@internal -def transfer_in(vault: Vault, other_vault: Vault, i: uint256, _from: address, amount: uint256) -> uint256: - token: ERC20 = empty(ERC20) - if i == 0: - token = ERC20(vault.borrowed_token()) - else: - token = ERC20(other_vault.borrowed_token()) - if amount > 0: - assert token.transferFrom(_from, self, amount, default_return_value=True) - if i == 1: - return other_vault.deposit(amount) - else: - return amount - else: - return 0 - - -@internal -def transfer_out(vault: Vault, other_vault: Vault, i: uint256, _to: address) -> uint256: - token: ERC20 = empty(ERC20) - amount: uint256 = 0 - if i == 0: - token = ERC20(vault.borrowed_token()) - amount = token.balanceOf(self) - if amount > 0: - assert token.transfer(_to, amount, default_return_value=True) - else: - token = ERC20(vault.collateral_token()) - amount = other_vault.redeem(other_vault.balanceOf(self), _to) - return amount - - -@external -@nonreentrant('lock') -def exchange(vault_id: uint256, i: uint256, j: uint256, amount: uint256, min_out: uint256, receiver: address = msg.sender) -> uint256[2]: - vault: Vault = self.vaults[vault_id] - other_vault: Vault = self.other_vault(vault_id) - _receiver: address = receiver - _min_out: uint256 = min_out - dx: uint256 = self.transfer_in(vault, other_vault, i, msg.sender, amount) - if j == 1: - _receiver = self - _min_out = 0 - dxy: uint256[2] = self.amms[vault_id].exchange(i, j, dx, _min_out, _receiver) - - # Transfer out any leftovers of token i - if dxy[0] != dx: - dxy[0] = self.transfer_out(vault, other_vault, i, receiver) - dxy[0] = amount - min(dxy[0], amount) # if someone makes an unexpected donation to the factory - could be that we spent nothing - # ..and of token j - if j == 1: - dxy[1] = self.transfer_out(vault, other_vault, j, receiver) - assert dxy[1] >= min_out, "Slippage" - return dxy - - -@external -@nonreentrant('lock') -def exchange_dy(vault_id: uint256, i: uint256, j: uint256, amount: uint256, max_in: uint256, receiver: address = msg.sender) -> uint256[2]: - vault: Vault = self.vaults[vault_id] - other_vault: Vault = self.other_vault(vault_id) - _receiver: address = receiver - _max_in: uint256 = self.transfer_in(vault, other_vault, i, msg.sender, max_in) - _amount: uint256 = amount - - # i=0, j=1: exchange borrowed to collateral, amount is amount of collateral - # i=1, j=0: exchange of collateral to borrowed, amount is amount of borrowed - - if j == 1: - _amount = other_vault.convertToShares(amount) - _receiver = self # We will redeem shares from the inside - dxy: uint256[2] = self.amms[vault_id].exchange_dy(i, j, _amount, _max_in, _receiver) - - # Transfer out any leftovers of token i - if dxy[0] != _max_in: - dxy[0] = self.transfer_out(vault, other_vault, i, receiver) # Transfer out the remainder - dxy[0] = max_in - min(dxy[0], max_in) # if someone makes an unexpected donation to the factory - could be that we spent nothing - - # And token j if it was not transferred by exchange_dy itself - if j == 1: - dxy[1] = self.transfer_out(vault, other_vault, j, receiver) - - return dxy diff --git a/contracts/lending/Vault.vy b/contracts/lending/Vault.vy index a96403b5..4d71a6e0 100644 --- a/contracts/lending/Vault.vy +++ b/contracts/lending/Vault.vy @@ -1,101 +1,46 @@ -# @version 0.3.10 +# pragma version 0.4.3 +# pragma optimize codesize """ -@title Vault -@notice ERC4626+ Vault for lending with crvUSD using LLAMMA algorithm +@title LlamaLend Vault +@notice ERC4626+ Vault for lending using LLAMMA algorithm @author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved """ -from vyper.interfaces import ERC20 as ERC20Spec -from vyper.interfaces import ERC20Detailed +from curve_std.interfaces import IERC20 +from curve_std.interfaces import IERC4626 +from contracts.interfaces import IAMM +from contracts.interfaces import ILlamalendController as IController +from contracts.interfaces import IFactory -implements: ERC20Spec -implements: ERC20Detailed +from contracts import constants as c +from curve_std import token as tkn +implements: IERC20 +implements: IERC4626 -interface ERC20: - def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable - def transfer(_to: address, _value: uint256) -> bool: nonpayable - def decimals() -> uint256: view - def balanceOf(_from: address) -> uint256: view - def symbol() -> String[32]: view - def name() -> String[64]: view - -interface AMM: - def set_admin(_admin: address): nonpayable - def rate() -> uint256: view - -interface Controller: - def total_debt() -> uint256: view - def monetary_policy() -> address: view - def check_lock() -> bool: view - def save_rate(): nonpayable - -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - -interface Factory: - def admin() -> address: view - - -# ERC20 events - -event Approval: - owner: indexed(address) - spender: indexed(address) - value: uint256 - -event Transfer: - sender: indexed(address) - receiver: indexed(address) - value: uint256 - -# ERC4626 events - -event Deposit: - sender: indexed(address) - owner: indexed(address) - assets: uint256 - shares: uint256 - -event Withdraw: - sender: indexed(address) - receiver: indexed(address) - owner: indexed(address) - assets: uint256 - shares: uint256 +# TODO move to own interface event SetMaxSupply: max_supply: uint256 - -# Limits -MIN_A: constant(uint256) = 2 -MAX_A: constant(uint256) = 10000 -MIN_FEE: constant(uint256) = 10**6 # 1e-12, still needs to be above 0 -MAX_FEE: constant(uint256) = 10**17 # 10% -MAX_LOAN_DISCOUNT: constant(uint256) = 5 * 10**17 -MIN_LIQUIDATION_DISCOUNT: constant(uint256) = 10**16 -ADMIN_FEE: constant(uint256) = 0 - # These are virtual shares from method proposed by OpenZeppelin # see: https://blog.openzeppelin.com/a-novel-defense-against-erc4626-inflation-attacks # and # https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol -DEAD_SHARES: constant(uint256) = 1000 +DEAD_SHARES: constant(uint256) = c.DEAD_SHARES MIN_ASSETS: constant(uint256) = 10000 -borrowed_token: public(ERC20) -collateral_token: public(ERC20) +borrowed_token: public(IERC20) +collateral_token: public(IERC20) -price_oracle: public(PriceOracle) -amm: public(AMM) -controller: public(Controller) -factory: public(Factory) +amm: public(IAMM) +controller: public(IController) +factory: public(IFactory) maxSupply: public(uint256) +net_deposits: public(int256) # ERC20 publics @@ -112,113 +57,37 @@ totalSupply: public(uint256) precision: uint256 - -@external -def __init__(): - """ - @notice Template for Vault implementation - """ - # The contract is made a "normal" template (not blueprint) so that we can get contract address before init - # This is needed if we want to create a rehypothecation dual-market with two vaults - # where vaults are collaterals of each other - self.borrowed_token = ERC20(0x0000000000000000000000000000000000000001) - - -@internal -@pure -def ln_int(_x: uint256) -> int256: - """ - @notice Logarithm ln() function based on log2. Not very gas-efficient but brief - """ - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - # This can be much more optimal but that's not important here - x: uint256 = _x - res: uint256 = 0 - for i in range(8): - t: uint256 = 2**(7 - i) - p: uint256 = 2**t - if x >= p * 10**18: - x /= p - res += t * 10**18 - d: uint256 = 10**18 - for i in range(59): # 18 decimals: math.log2(10**10) == 59.7 - if (x >= 2 * 10**18): - res += d - x /= 2 - x = x * x / 10**18 - d /= 2 - # Now res = log2(x) - # ln(x) = log2(x) / log2(e) - return convert(res * 10**18 / 1442695040888963328, int256) - +# Only needed for initialize +interface IERC20Symbol: + def symbol() -> String[32]: view @external def initialize( - amm_impl: address, - controller_impl: address, - borrowed_token: ERC20, - collateral_token: ERC20, - A: uint256, - fee: uint256, - price_oracle: PriceOracle, # Factory makes from template if needed, deploying with a from_pool() - monetary_policy: address, # Standard monetary policy set in factory - loan_discount: uint256, - liquidation_discount: uint256 - ) -> (address, address): + amm: IAMM, + controller: IController, + borrowed_token: IERC20, + collateral_token: IERC20, + ): """ @notice Initializer for vaults - @param amm_impl AMM implementation (blueprint) - @param controller_impl Controller implementation (blueprint) + @param amm Address of the AMM + @param controller Address of the Controller @param borrowed_token Token which is being borrowed - @param collateral_token Token used for collateral - @param A Amplification coefficient: band size is ~1/A - @param fee Fee for swaps in AMM (for ETH markets found to be 0.6%) - @param price_oracle Already initialized price oracle - @param monetary_policy Already initialized monetary policy - @param loan_discount Maximum discount. LTV = sqrt(((A - 1) / A) ** 4) - loan_discount - @param liquidation_discount Liquidation discount. LT = sqrt(((A - 1) / A) ** 4) - liquidation_discount + @param collateral_token Token which is being collateral """ assert self.borrowed_token.address == empty(address) self.borrowed_token = borrowed_token + borrowed_precision: uint256 = 10**(18 - convert(staticcall borrowed_token.decimals(), uint256)) self.collateral_token = collateral_token - self.price_oracle = price_oracle - - assert A >= MIN_A and A <= MAX_A, "Wrong A" - assert fee <= MAX_FEE, "Fee too high" - assert fee >= MIN_FEE, "Fee too low" - assert liquidation_discount >= MIN_LIQUIDATION_DISCOUNT, "Liquidation discount too low" - assert loan_discount <= MAX_LOAN_DISCOUNT, "Loan discount too high" - assert loan_discount > liquidation_discount, "need loan_discount>liquidation_discount" - - p: uint256 = price_oracle.price() # This also validates price oracle ABI - assert p > 0 - assert price_oracle.price_w() == p - A_ratio: uint256 = 10**18 * A / (A - 1) - - borrowed_precision: uint256 = 10**(18 - borrowed_token.decimals()) - - amm: address = create_from_blueprint( - amm_impl, - borrowed_token.address, borrowed_precision, - collateral_token.address, 10**(18 - collateral_token.decimals()), - A, isqrt(A_ratio * 10**18), self.ln_int(A_ratio), - p, fee, ADMIN_FEE, price_oracle.address, - code_offset=3) - controller: address = create_from_blueprint( - controller_impl, - empty(address), monetary_policy, loan_discount, liquidation_discount, amm, - code_offset=3) - AMM(amm).set_admin(controller) - - self.amm = AMM(amm) - self.controller = Controller(controller) - self.factory = Factory(msg.sender) + + self.factory = IFactory(msg.sender) + self.amm = amm + self.controller = controller # ERC20 set up self.precision = borrowed_precision - borrowed_symbol: String[32] = borrowed_token.symbol() + borrowed_symbol: String[32] = staticcall IERC20Symbol(borrowed_token.address).symbol() self.name = concat(NAME_PREFIX, borrowed_symbol) # Symbol must be String[32], but we do String[34]. It doesn't affect contracts which read it (they will truncate) # However this will be changed as soon as Vyper can *properly* manipulate strings @@ -226,48 +95,43 @@ def initialize( self.maxSupply = max_value(uint256) - # No events because it's the only market we would ever create in this contract - - return controller, amm - @external def set_max_supply(max_supply: uint256): """ @notice Set maximum depositable supply """ - assert msg.sender == self.factory.admin() or msg.sender == self.factory.address + assert msg.sender == staticcall self.factory.admin() or msg.sender == self.factory.address self.maxSupply = max_supply - log SetMaxSupply(max_supply) + log SetMaxSupply(max_supply=max_supply) @external @view -@nonreentrant('lock') +@nonreentrant def borrow_apr() -> uint256: """ @notice Borrow APR (annualized and 1e18-based) """ - return self.amm.rate() * (365 * 86400) + return staticcall self.amm.rate() * (365 * 86400) @external @view -@nonreentrant('lock') +@nonreentrant def lend_apr() -> uint256: """ @notice Lending APR (annualized and 1e18-based) """ - debt: uint256 = self.controller.total_debt() + debt: uint256 = staticcall self.controller.total_debt() if debt == 0: return 0 - else: - return self.amm.rate() * (365 * 86400) * debt / self._total_assets() + return staticcall self.amm.rate() * (365 * 86400) * debt // self._total_assets() @external @view -def asset() -> ERC20: +def asset() -> IERC20: """ @notice Asset which is the same as borrowed_token """ @@ -278,13 +142,12 @@ def asset() -> ERC20: @view def _total_assets() -> uint256: # admin fee should be accounted for here when enabled - self.controller.check_lock() - return self.borrowed_token.balanceOf(self.controller.address) + self.controller.total_debt() + return staticcall self.controller.available_balance() + staticcall self.controller.total_debt() @external @view -@nonreentrant('lock') +@nonreentrant def totalAssets() -> uint256: """ @notice Total assets which can be lent out or be in reserve @@ -303,9 +166,9 @@ def _convert_to_shares(assets: uint256, is_floor: bool = True, numerator: uint256 = (self.totalSupply + DEAD_SHARES) * assets * precision denominator: uint256 = (total_assets * precision + 1) if is_floor: - return numerator / denominator + return numerator // denominator else: - return (numerator + denominator - 1) / denominator + return (numerator + denominator - 1) // denominator @internal @@ -319,37 +182,37 @@ def _convert_to_assets(shares: uint256, is_floor: bool = True, numerator: uint256 = shares * (total_assets * precision + 1) denominator: uint256 = (self.totalSupply + DEAD_SHARES) * precision if is_floor: - return numerator / denominator + return numerator // denominator else: - return (numerator + denominator - 1) / denominator + return (numerator + denominator - 1) // denominator @external @view -@nonreentrant('lock') +@nonreentrant def pricePerShare(is_floor: bool = True) -> uint256: """ @notice Method which shows how much one pool share costs in asset tokens if they are normalized to 18 decimals """ supply: uint256 = self.totalSupply if supply == 0: - return 10**18 / DEAD_SHARES + return 10**18 // DEAD_SHARES else: precision: uint256 = self.precision numerator: uint256 = 10**18 * (self._total_assets() * precision + 1) denominator: uint256 = (supply + DEAD_SHARES) pps: uint256 = 0 if is_floor: - pps = numerator / denominator + pps = numerator // denominator else: - pps = (numerator + denominator - 1) / denominator + pps = (numerator + denominator - 1) // denominator assert pps > 0 return pps @external @view -@nonreentrant('lock') +@nonreentrant def convertToShares(assets: uint256) -> uint256: """ @notice Returns the amount of shares which the Vault would exchange for the given amount of shares provided @@ -359,7 +222,7 @@ def convertToShares(assets: uint256) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def convertToAssets(shares: uint256) -> uint256: """ @notice Returns the amount of assets that the Vault would exchange for the amount of shares provided @@ -383,7 +246,7 @@ def maxDeposit(receiver: address) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def previewDeposit(assets: uint256) -> uint256: """ @notice Returns the amount of shares which can be obtained upon depositing assets @@ -392,22 +255,23 @@ def previewDeposit(assets: uint256) -> uint256: @external -@nonreentrant('lock') +@nonreentrant def deposit(assets: uint256, receiver: address = msg.sender) -> uint256: """ @notice Deposit assets in return for whatever number of shares corresponds to the current conditions @param assets Amount of assets to deposit @param receiver Receiver of the shares who is optional. If not specified - receiver is the sender """ - controller: Controller = self.controller + controller: IController = self.controller total_assets: uint256 = self._total_assets() assert total_assets + assets >= MIN_ASSETS, "Need more assets" assert total_assets + assets <= self.maxSupply, "Supply limit" to_mint: uint256 = self._convert_to_shares(assets, True, total_assets) - assert self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) + tkn.transfer_from(self.borrowed_token, msg.sender, controller.address, assets) + self.net_deposits += convert(assets, int256) self._mint(receiver, to_mint) - controller.save_rate() - log Deposit(msg.sender, receiver, assets, to_mint) + extcall controller.save_rate() + log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=to_mint) return to_mint @@ -427,7 +291,7 @@ def maxMint(receiver: address) -> uint256: @external @view -@nonreentrant('lock') +@nonreentrant def previewMint(shares: uint256) -> uint256: """ @notice Calculate the amount of assets which is needed to exactly mint the given amount of shares @@ -436,50 +300,51 @@ def previewMint(shares: uint256) -> uint256: @external -@nonreentrant('lock') +@nonreentrant def mint(shares: uint256, receiver: address = msg.sender) -> uint256: """ @notice Mint given amount of shares taking whatever number of assets it requires @param shares Number of sharess to mint @param receiver Optional receiver for the shares. If not specified - it's the sender """ - controller: Controller = self.controller + controller: IController = self.controller total_assets: uint256 = self._total_assets() assets: uint256 = self._convert_to_assets(shares, False, total_assets) assert total_assets + assets >= MIN_ASSETS, "Need more assets" assert total_assets + assets <= self.maxSupply, "Supply limit" - assert self.borrowed_token.transferFrom(msg.sender, controller.address, assets, default_return_value=True) + tkn.transfer_from(self.borrowed_token, msg.sender, controller.address, assets) + self.net_deposits += convert(assets, int256) self._mint(receiver, shares) - controller.save_rate() - log Deposit(msg.sender, receiver, assets, shares) + extcall controller.save_rate() + log IERC4626.Deposit(sender=msg.sender, owner=receiver, assets=assets, shares=shares) return assets @external @view -@nonreentrant('lock') +@nonreentrant def maxWithdraw(owner: address) -> uint256: """ @notice Maximum amount of assets which a given user can withdraw. Aware of both user's balance and available liquidity """ return min( self._convert_to_assets(self.balanceOf[owner]), - self.borrowed_token.balanceOf(self.controller.address)) + staticcall self.controller.available_balance()) @external @view -@nonreentrant('lock') +@nonreentrant def previewWithdraw(assets: uint256) -> uint256: """ @notice Calculate number of shares which gets burned when withdrawing given amount of asset """ - assert assets <= self.borrowed_token.balanceOf(self.controller.address) + assert assets <= staticcall self.controller.available_balance() return self._convert_to_shares(assets, False) @external -@nonreentrant('lock') +@nonreentrant def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256: """ @notice Withdraw given amount of asset and burn the corresponding amount of vault shares @@ -495,29 +360,30 @@ def withdraw(assets: uint256, receiver: address = msg.sender, owner: address = m if allowance != max_value(uint256): self._approve(owner, msg.sender, allowance - shares) - controller: Controller = self.controller + controller: IController = self.controller self._burn(owner, shares) - assert self.borrowed_token.transferFrom(controller.address, receiver, assets, default_return_value=True) - controller.save_rate() - log Withdraw(msg.sender, receiver, owner, assets, shares) + tkn.transfer_from(self.borrowed_token, controller.address, receiver, assets) + self.net_deposits -= convert(assets, int256) + extcall controller.save_rate() + log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets, shares=shares) return shares @external @view -@nonreentrant('lock') +@nonreentrant def maxRedeem(owner: address) -> uint256: """ @notice Calculate maximum amount of shares which a given user can redeem """ return min( - self._convert_to_shares(self.borrowed_token.balanceOf(self.controller.address), False), + self._convert_to_shares(staticcall self.controller.available_balance(), False), self.balanceOf[owner]) @external @view -@nonreentrant('lock') +@nonreentrant def previewRedeem(shares: uint256) -> uint256: """ @notice Calculate the amount of assets which can be obtained by redeeming the given amount of shares @@ -528,12 +394,13 @@ def previewRedeem(shares: uint256) -> uint256: else: assets_to_redeem: uint256 = self._convert_to_assets(shares) - assert assets_to_redeem <= self.borrowed_token.balanceOf(self.controller.address) + # TODO can't revert here + assert assets_to_redeem <= staticcall self.controller.available_balance() return assets_to_redeem @external -@nonreentrant('lock') +@nonreentrant def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg.sender) -> uint256: """ @notice Burn given amount of shares and give corresponding assets to the user @@ -555,10 +422,11 @@ def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg else: raise "Need more assets" self._burn(owner, shares) - controller: Controller = self.controller - assert self.borrowed_token.transferFrom(controller.address, receiver, assets_to_redeem, default_return_value=True) - controller.save_rate() - log Withdraw(msg.sender, receiver, owner, assets_to_redeem, shares) + controller: IController = self.controller + tkn.transfer_from(self.borrowed_token, controller.address, receiver, assets_to_redeem) + self.net_deposits -= convert(assets_to_redeem, int256) + extcall controller.save_rate() + log IERC4626.Withdraw(sender=msg.sender, receiver=receiver, owner=owner, assets=assets_to_redeem, shares=shares) return assets_to_redeem @@ -568,7 +436,7 @@ def redeem(shares: uint256, receiver: address = msg.sender, owner: address = msg def _approve(_owner: address, _spender: address, _value: uint256): self.allowance[_owner][_spender] = _value - log Approval(_owner, _spender, _value) + log IERC20.Approval(owner=_owner, spender=_spender, value=_value) @internal @@ -576,7 +444,7 @@ def _burn(_from: address, _value: uint256): self.balanceOf[_from] -= _value self.totalSupply -= _value - log Transfer(_from, empty(address), _value) + log IERC20.Transfer(sender=_from, receiver=empty(address), value=_value) @internal @@ -584,7 +452,7 @@ def _mint(_to: address, _value: uint256): self.balanceOf[_to] += _value self.totalSupply += _value - log Transfer(empty(address), _to, _value) + log IERC20.Transfer(sender=empty(address), receiver=_to, value=_value) @internal @@ -594,7 +462,7 @@ def _transfer(_from: address, _to: address, _value: uint256): self.balanceOf[_from] -= _value self.balanceOf[_to] += _value - log Transfer(_from, _to, _value) + log IERC20.Transfer(sender=_from, receiver=_to, value=_value) @external @@ -689,4 +557,4 @@ def decreaseAllowance(_spender: address, _sub_value: uint256) -> bool: @external @view def admin() -> address: - return self.factory.admin() + return staticcall self.factory.admin() diff --git a/contracts/lib/liquidation_lib.vy b/contracts/lib/liquidation_lib.vy new file mode 100644 index 00000000..9e3fe31f --- /dev/null +++ b/contracts/lib/liquidation_lib.vy @@ -0,0 +1,44 @@ +from contracts.interfaces import IController +from contracts.interfaces import IAMM + + +@internal +@view +def users_with_health( + controller: IController, + _from: uint256, + _limit: uint256, + threshold: int256, + require_approval: bool, + approval_spender: address, + full: bool, +) -> DynArray[IController.Position, 1000]: + """ + Enumerate controller loans and return positions with health < threshold. + Optionally require controller.approval(user, approval_spender). + Returns IController.Position entries (user, x, y, debt, health). + """ + AMM: IAMM = staticcall controller.amm() + + n_loans: uint256 = staticcall controller.n_loans() + limit: uint256 = _limit if _limit != 0 else n_loans + ix: uint256 = _from + out: DynArray[IController.Position, 1000] = [] + for i: uint256 in range(10**6): + if ix >= n_loans or i == limit: + break + user: address = staticcall controller.loans(ix) + h: int256 = staticcall controller.health(user, full) + ok: bool = h < threshold + if ok and require_approval: + ok = staticcall controller.approval(user, approval_spender) + if ok: + xy: uint256[2] = staticcall AMM.get_sum_xy(user) + debt: uint256 = staticcall controller.debt(user) + out.append( + IController.Position( + user=user, x=xy[0], y=xy[1], debt=debt, health=h + ) + ) + ix += 1 + return out diff --git a/contracts/mpolicies/SemilogMonetaryPolicy.vy b/contracts/mpolicies/SemilogMonetaryPolicy.vy index 9c843b14..fa7b1f02 100644 --- a/contracts/mpolicies/SemilogMonetaryPolicy.vy +++ b/contracts/mpolicies/SemilogMonetaryPolicy.vy @@ -10,6 +10,7 @@ @author Curve.fi @license Copyright (c) Curve.Fi, 2020-2024 - all rights reserved """ +# TODO bump to vyper 0.4.3 from vyper.interfaces import ERC20 @@ -39,7 +40,12 @@ log_max_rate: public(int256) @external -def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256): +def __init__( + borrowed_token: ERC20, + min_rate: uint256, + max_rate: uint256, + _factory: address +): assert min_rate >= MIN_RATE and max_rate <= MAX_RATE and min_rate <= max_rate, "Wrong rates" BORROWED_TOKEN = borrowed_token @@ -48,7 +54,7 @@ def __init__(borrowed_token: ERC20, min_rate: uint256, max_rate: uint256): self.log_min_rate = self.ln_int(min_rate) self.log_max_rate = self.ln_int(max_rate) - FACTORY = Factory(msg.sender) + FACTORY = Factory(_factory) ### MATH ### diff --git a/contracts/price_oracles/CryptoFromPoolsRate.vy b/contracts/price_oracles/CryptoFromPoolsRate.vy index 3a83688e..d5cb5362 100644 --- a/contracts/price_oracles/CryptoFromPoolsRate.vy +++ b/contracts/price_oracles/CryptoFromPoolsRate.vy @@ -50,6 +50,7 @@ def __init__( break # Find N + # TODO duplicate code to modularize N: uint256 = 0 for j in range(MAX_COINS + 1): success: bool = False diff --git a/contracts/price_oracles/CryptoFromPoolsRateWAgg.vy b/contracts/price_oracles/CryptoFromPoolsRateWAgg.vy index f4b4ad41..813bfa97 100644 --- a/contracts/price_oracles/CryptoFromPoolsRateWAgg.vy +++ b/contracts/price_oracles/CryptoFromPoolsRateWAgg.vy @@ -59,6 +59,7 @@ def __init__( # Find N N: uint256 = 0 + # TODO duplicate code to modularize for j in range(MAX_COINS + 1): success: bool = False res: Bytes[32] = empty(Bytes[32]) diff --git a/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy b/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy index b5f6b7dc..5fb40060 100644 --- a/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy +++ b/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy @@ -64,6 +64,7 @@ def __init__( # Find N N: uint256 = 0 + # TODO duplicate code to modularize for j in range(MAX_COINS + 1): success: bool = False res: Bytes[32] = empty(Bytes[32]) diff --git a/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy b/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy index 9d9fee6a..e483f422 100644 --- a/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy +++ b/contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy @@ -72,6 +72,7 @@ def __init__( # Find N N: uint256 = 0 + # TODO duplicate code to modularize for j in range(MAX_COINS + 1): success: bool = False res: Bytes[32] = empty(Bytes[32]) diff --git a/contracts/price_oracles/L2/CryptoFromPoolsWAgg.vy b/contracts/price_oracles/L2/CryptoFromPoolsWAgg.vy index 7484c783..cf741adc 100644 --- a/contracts/price_oracles/L2/CryptoFromPoolsWAgg.vy +++ b/contracts/price_oracles/L2/CryptoFromPoolsWAgg.vy @@ -65,6 +65,7 @@ def __init__( break # Find N + # TODO duplicate code to modularize N: uint256 = 0 for j: uint256 in range(MAX_COINS + 1): success: bool = False diff --git a/contracts/price_oracles/LPOracle.vy b/contracts/price_oracles/LPOracle.vy deleted file mode 100644 index 9626652b..00000000 --- a/contracts/price_oracles/LPOracle.vy +++ /dev/null @@ -1,131 +0,0 @@ -# @version 0.4.1 -#pragma optimize gas -#pragma evm-version shanghai - -""" -@title LPOracle -@author Curve.Fi -@license GNU Affero General Public License v3.0 only -@notice Price oracle for Curve pool LPs. First, the oracle gets LP token price in terms of the first coin (coin0) of the pool. - Then it chains with another oracle (target_coin/coin0) to get the final price. -""" - -from ethereum.ercs import IERC20Detailed - - -interface Pool: - def coins(i: uint256) -> address: view - def price_oracle(i: uint256 = 0) -> uint256: view # Universal method! - def stored_rates() -> DynArray[uint256, MAX_COINS]: view - def get_virtual_price() -> uint256: view - def lp_price() -> uint256: view # Exists only for cryptopools - -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - - -MAX_COINS: constant(uint256) = 8 - -POOL: public(immutable(Pool)) -COIN0_ORACLE: public(immutable(PriceOracle)) -IS_CRYPTO: public(immutable(bool)) -NO_ARGUMENT: public(immutable(bool)) -N_COINS: public(immutable(uint256)) -PRECISIONS: public(immutable(DynArray[uint256, MAX_COINS])) - - -@deploy -def __init__(pool: Pool, coin0_oracle: PriceOracle): - is_crypto: bool = False - no_argument: bool = False - precisions: DynArray[uint256, MAX_COINS] = empty(DynArray[uint256, MAX_COINS]) - - # Init variables for raw calls - res: Bytes[1024] = empty(Bytes[1024]) - success: bool = False - - success, res = raw_call(pool.address, method_id("lp_price()"), max_outsize=32, is_static_call=True, revert_on_failure=False) - if success and len(res) > 0: - is_crypto = True - - # Find N_COINS and store PRECISIONS - for i: uint256 in range(MAX_COINS + 1): - success, res = raw_call( - pool.address, - abi_encode(i, method_id=method_id("coins(uint256)")), - max_outsize=32, is_static_call=True, revert_on_failure=False) - if not success: - assert i != 0, "No coins(0)" - N_COINS = i - break - - coin: IERC20Detailed = IERC20Detailed(abi_decode(res, address)) - if coin.address != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE: - precisions.append(10**(18 - convert(staticcall coin.decimals(), uint256))) - else: - precisions.append(1) - - # Check and record if pool requires coin id in argument or no - if N_COINS == 2: - success, res = raw_call( - pool.address, - abi_encode(empty(uint256), method_id=method_id("price_oracle(uint256)")), - max_outsize=32, is_static_call=True, revert_on_failure=False) - if not success: - no_argument = True - - POOL = pool - COIN0_ORACLE = coin0_oracle - IS_CRYPTO = is_crypto - NO_ARGUMENT = no_argument - PRECISIONS = precisions - - -@internal -@view -def _coin0_oracle_price() -> uint256: - if COIN0_ORACLE.address != empty(address): - return staticcall COIN0_ORACLE.price() - else: - return 10**18 - - -@internal -def _coin0_oracle_price_w() -> uint256: - if COIN0_ORACLE.address != empty(address): - return extcall COIN0_ORACLE.price_w() - else: - return 10**18 - - -@internal -@view -def _price_in_coin0() -> uint256: - if IS_CRYPTO: - return staticcall POOL.lp_price() - - min_p: uint256 = max_value(uint256) - for i: uint256 in range(N_COINS, bound=MAX_COINS): - p_oracle: uint256 = 10 ** 18 - if i > 0: - if NO_ARGUMENT: - p_oracle = staticcall POOL.price_oracle() - else: - p_oracle = staticcall POOL.price_oracle(unsafe_sub(i, 1)) - - if p_oracle < min_p: - min_p = p_oracle - - return min_p * (staticcall POOL.get_virtual_price()) // 10**18 - - -@external -@view -def price() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price() // 10 ** 18 - - -@external -def price_w() -> uint256: - return self._price_in_coin0() * self._coin0_oracle_price_w() // 10 ** 18 diff --git a/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy new file mode 100644 index 00000000..4eae8944 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/LPOracleCrypto.vy @@ -0,0 +1,46 @@ +# pragma version 0.4.3 +""" +@title LPOracleCrypto +@author Curve.Fi +@license MIT +@notice Price oracle for Curve Crypto Pool LPs. First, the oracle gets LP token price in terms of the first coin (coin0) of the pool. + Then it chains with another oracle (target_coin/coin0) to get the final price. +""" +from contracts.interfaces import IPriceOracle +from contracts.interfaces import ICryptoPool +from contracts import constants as c + + +import lp_oracle_lib +initializes: lp_oracle_lib +exports: lp_oracle_lib.COIN0_ORACLE + + +POOL: public(immutable(ICryptoPool)) + + +@deploy +def __init__(_pool: ICryptoPool, _coin0_oracle: IPriceOracle): + assert staticcall _pool.lp_price() > 0, "pool.lp_price() returns 0" + if _coin0_oracle.address != empty(address): + assert staticcall _coin0_oracle.price() > 0, "coin0_oracle.price() returns 0" + assert extcall _coin0_oracle.price_w() > 0, "coin0_oracle.price_w() returns 0" + POOL = _pool + lp_oracle_lib.__init__(_coin0_oracle) + + +@internal +@view +def _price_in_coin0() -> uint256: + return staticcall POOL.lp_price() + + +@external +@view +def price() -> uint256: + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price() // c.WAD + + +@external +def price_w() -> uint256: + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price_w() // c.WAD diff --git a/contracts/price_oracles/lp-oracles/LPOracleFactory.vy b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy new file mode 100644 index 00000000..acf2ac5b --- /dev/null +++ b/contracts/price_oracles/lp-oracles/LPOracleFactory.vy @@ -0,0 +1,105 @@ +# @version 0.4.3 +#pragma optimize gas +#pragma evm-version shanghai + +""" +@title LPOracleFactory +@author Curve.Fi +@license GNU Affero General Public License v3.0 only +@notice Permissionless StablePool and CryptoPool LP Oracles deployer and registry +""" + + +from snekmate.auth import ownable +initializes: ownable +exports: ownable.__interface__ + + +interface IProxyOracleFactory: + def deploy_proxy_oracle(_oracle: address) -> address: nonpayable + + +event DeployOracle: + oracle: indexed(address) + pool: indexed(address) + coin0_oracle: address + + +struct OracleInfo: + pool: address + coin0_oracle: address + + +oracle_map: HashMap[address, HashMap[address, address]] # oracle_map[pool][coin0_oracle] -> oracle +oracle_info: public(HashMap[address, OracleInfo]) # oracle_info[oracle] -> OracleInfo + +STABLE_IMPLEMENTATION: public(immutable(address)) +CRYPTO_IMPLEMENTATION: public(immutable(address)) +PROXY_ORACLE_FACTORY: public(immutable(IProxyOracleFactory)) + + +@deploy +def __init__(_admin: address, _stable_implementation: address, _crypto_implementation: address, _proxy_oracle_factory: IProxyOracleFactory): + """ + @notice Factory which creates StablePool and CryptoPool LP Oracles from blueprints + @param admin Admin of the factory (ideally DAO) + """ + ownable.__init__() + ownable._transfer_ownership(_admin) + + assert _stable_implementation != empty(address) + assert _crypto_implementation != empty(address) + assert _proxy_oracle_factory.address != empty(address) + STABLE_IMPLEMENTATION = _stable_implementation + CRYPTO_IMPLEMENTATION = _crypto_implementation + PROXY_ORACLE_FACTORY = _proxy_oracle_factory + + +@external +@nonreentrant +def deploy_oracle(_pool: address, _coin0_oracle: address, _use_proxy: bool = True) -> address[2]: + """ + @notice Deploy a new LP oracle + @param _pool Curve pool either stable or crypto + @param _coin0_oracle Oracle for the first coin of the pool + @param _use_proxy Whether to deploy proxy oracle or not + @return [deployed oracle address, deployed proxy address or zero] + """ + assert self.oracle_map[_pool][_coin0_oracle] == empty(address), "Oracle already exists" + + implementation: address = STABLE_IMPLEMENTATION + if self._is_crypto(_pool): + implementation = CRYPTO_IMPLEMENTATION + oracle: address = create_from_blueprint(implementation, _pool, _coin0_oracle, code_offset=3) + proxy: address = empty(address) + if _use_proxy: + proxy = extcall PROXY_ORACLE_FACTORY.deploy_proxy_oracle(oracle) + self.oracle_map[_pool][_coin0_oracle] = oracle + self.oracle_info[oracle] = OracleInfo(pool=_pool, coin0_oracle=_coin0_oracle) + + log DeployOracle(oracle=oracle, pool=_pool, coin0_oracle=_coin0_oracle) + + return [oracle, proxy] + + +@external +@view +def get_oracle(_pool: address, _coin0_oracle: address) -> address: + """ + @param _pool Curve pool either stable or crypto + @param _coin0_oracle Oracle for the first coin of the pool + @return Oracle address + """ + return self.oracle_map[_pool][_coin0_oracle] + + +@internal +@view +def _is_crypto(pool: address) -> bool: + success: bool = False + res: Bytes[32] = empty(Bytes[32]) + success, res = raw_call(pool, method_id("lp_price()"), max_outsize=32, is_static_call=True, revert_on_failure=False) + if success and len(res) > 0: + return True + + return False diff --git a/contracts/price_oracles/lp-oracles/LPOracleStable.vy b/contracts/price_oracles/lp-oracles/LPOracleStable.vy new file mode 100644 index 00000000..50b291d7 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/LPOracleStable.vy @@ -0,0 +1,94 @@ +# pragma version 0.4.3 +""" +@title LPOracleStable +@author Curve.Fi +@license MIT +@notice Price oracle for Curve Stable Pool LPs. First, the oracle gets LP token price in terms of the first coin (coin0) of the pool. + Then it chains with another oracle (target_coin/coin0) to get the final price. +""" + +from contracts.interfaces import IPriceOracle +from contracts.interfaces import IStablePool +from contracts import constants as c + + +import lp_oracle_lib +initializes: lp_oracle_lib +exports: lp_oracle_lib.COIN0_ORACLE + +MAX_COINS: constant(uint256) = 8 + +POOL: public(immutable(IStablePool)) +NO_ARGUMENT: public(immutable(bool)) +N_COINS: public(immutable(uint256)) + + +@deploy +def __init__(_pool: IStablePool, _coin0_oracle: IPriceOracle): + no_argument: bool = False + + # Init variables for raw calls + res: Bytes[32] = empty(Bytes[32]) + success: bool = False + + # Find N_COINS + # TODO duplicate code to modularize + for i: uint256 in range(MAX_COINS + 1): + success, res = raw_call( + _pool.address, + abi_encode(i, method_id=method_id("coins(uint256)")), + max_outsize=32, is_static_call=True, revert_on_failure=False) + if not success: + assert i > 1, "Less than 2 coins" + N_COINS = i + break + + # Check price_oracle() and record if the method requires coin id in argument or no + for i: uint256 in range(N_COINS - 1, bound=MAX_COINS): + success, res = raw_call( + _pool.address, + abi_encode(i, method_id=method_id("price_oracle(uint256)")), + max_outsize=32, is_static_call=True, revert_on_failure=False) + if success: + assert convert(res, uint256) > 0, "pool.price_oracle(i) returns 0" + else: + assert i == 0 and N_COINS == 2, "no argument for coins > 2" + assert staticcall _pool.price_oracle() > 0, "pool.price_oracle() returns 0" + no_argument = True + + if _coin0_oracle.address != empty(address): + assert staticcall _coin0_oracle.price() > 0, "coin0_oracle.price() returns 0" + assert extcall _coin0_oracle.price_w() > 0, "coin0_oracle.price_w() returns 0" + + POOL = _pool + NO_ARGUMENT = no_argument + lp_oracle_lib.__init__(_coin0_oracle) + + +@internal +@view +def _price_in_coin0() -> uint256: + min_p: uint256 = max_value(uint256) + for i: uint256 in range(N_COINS, bound=MAX_COINS): + p_oracle: uint256 = c.WAD + if i > 0: + if NO_ARGUMENT: + p_oracle = staticcall POOL.price_oracle() + else: + p_oracle = staticcall POOL.price_oracle(unsafe_sub(i, 1)) + + if p_oracle < min_p: + min_p = p_oracle + + return min_p * (staticcall POOL.get_virtual_price()) // c.WAD + + +@external +@view +def price() -> uint256: + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price() // c.WAD + + +@external +def price_w() -> uint256: + return self._price_in_coin0() * lp_oracle_lib._coin0_oracle_price_w() // c.WAD diff --git a/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy new file mode 100644 index 00000000..4a8edbe7 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/lp_oracle_lib.vy @@ -0,0 +1,28 @@ +# pragma version 0.4.3 +from contracts.interfaces import IPriceOracle +from contracts import constants as c + + +COIN0_ORACLE: public(immutable(IPriceOracle)) + + +@deploy +def __init__(coin0_oracle: IPriceOracle): + COIN0_ORACLE = coin0_oracle + + +@internal +@view +def _coin0_oracle_price() -> uint256: + if COIN0_ORACLE.address != empty(address): + return staticcall COIN0_ORACLE.price() + else: + return c.WAD + + +@internal +def _coin0_oracle_price_w() -> uint256: + if COIN0_ORACLE.address != empty(address): + return extcall COIN0_ORACLE.price_w() + else: + return c.WAD diff --git a/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy b/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy new file mode 100644 index 00000000..6dd76f06 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/testing/MockCryptoSwap.vy @@ -0,0 +1,21 @@ +# @version 0.4.1 + +""" +This contract is for testing only. +If you see it on mainnet - it won't be used for anything except testing the actual deployment +""" + +lp_price: public(uint256) +ADMIN: immutable(address) + + +@deploy +def __init__(_admin: address, _lp_price: uint256): + self.lp_price = _lp_price + ADMIN = _admin + + +@external +def set_lp_price(_lp_price: uint256): + assert msg.sender == ADMIN + self.lp_price = _lp_price diff --git a/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy b/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy new file mode 100644 index 00000000..47948cc4 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwap.vy @@ -0,0 +1,31 @@ +# @version 0.4.1 + +""" +This contract is for testing only. +If you see it on mainnet - it won't be used for anything except testing the actual deployment +""" + +MAX_COINS: constant(uint256) = 8 + +ADMIN: immutable(address) +coins: public(immutable(DynArray[address, MAX_COINS])) +price_oracle: public(DynArray[uint256, MAX_COINS - 1]) +get_virtual_price: public(uint256) + + +@deploy +def __init__(_admin: address, _prices: DynArray[uint256, MAX_COINS - 1]): + ADMIN = _admin + self.price_oracle = _prices + _coins: DynArray[address, MAX_COINS] = [] + for i: uint256 in range(len(_prices) + 1, bound=MAX_COINS): + _coins.append(empty(address)) + + coins = _coins + self.get_virtual_price = 10**18 + + +@external +def set_price(_i: uint256, _price: uint256): + assert msg.sender == ADMIN + self.price_oracle[_i] = _price diff --git a/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy b/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy new file mode 100644 index 00000000..2d9cf920 --- /dev/null +++ b/contracts/price_oracles/lp-oracles/testing/MockStableSwapNoArgument.vy @@ -0,0 +1,27 @@ +# @version 0.4.1 + +""" +This contract is for testing only. +If you see it on mainnet - it won't be used for anything except testing the actual deployment +""" + +MAX_COINS: constant(uint256) = 2 + +ADMIN: immutable(address) +coins: public(immutable(address[2])) +price_oracle: public(uint256) +get_virtual_price: public(uint256) + + +@deploy +def __init__(_admin: address, _price: uint256): + ADMIN = _admin + self.price_oracle = _price + coins = [empty(address), empty(address)] + self.get_virtual_price = 10**18 + + +@external +def set_price(_price: uint256): + assert msg.sender == ADMIN + self.price_oracle = _price diff --git a/contracts/price_oracles/proxy/ProxyOracle.vy b/contracts/price_oracles/proxy/ProxyOracle.vy new file mode 100644 index 00000000..266a5215 --- /dev/null +++ b/contracts/price_oracles/proxy/ProxyOracle.vy @@ -0,0 +1,133 @@ +# @version 0.4.3 +#pragma optimize gas +#pragma evm-version shanghai + +""" +@title ProxyOracle +@author Curve +@license MIT +@notice Proxy oracle allowing LlamaLend factory admin to set price oracle contract after deployment +""" + +# TODO make more idiomatic +# TODO align with Llamalend V2 behavior (possibly modules)? + +interface IFactory: + def owner() -> address: view + +interface IPriceOracle: + def price() -> uint256: view + def price_w() -> uint256: nonpayable + + +event PriceOracleSet: + oracle: address + +event MaxDeviationSet: + max_deviation: uint256 + + +MAX_DEVIATION_BPS: constant(uint256) = 5000 # 50% + +factory: public(IFactory) +oracle: public(IPriceOracle) +max_deviation: public(uint256) + + +@deploy +def __init__(): + """ + @notice Template for OracleProxy implementation + """ + self.oracle = IPriceOracle(0x0000000000000000000000000000000000000001) + + +@external +def initialize(_oracle: IPriceOracle, _max_deviation: uint256 = 100): + """ + @notice Initializer for Llamalend oracle proxy + @param _oracle Oracle contract + @param _max_deviation Max price deviation when setting new oracle, in BPS (e.g. 500 == 5%) + """ + assert self.oracle.address == empty(address), "Already initialized" + assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid max deviation" + self._validate_price_oracle(_oracle) + + self.oracle = _oracle + self.max_deviation = _max_deviation + self.factory = IFactory(msg.sender) + + log PriceOracleSet(oracle=_oracle.address) + log MaxDeviationSet(max_deviation=_max_deviation) + + +@internal +def _validate_price_oracle(_oracle: IPriceOracle) -> uint256: + """ + @notice Validates the new oracle has methods implemented correctly + """ + assert _oracle.address != empty(address), "Invalid address" + + block_price: uint256 = extcall _oracle.price_w() + assert staticcall _oracle.price() > 0, "price() call failed" + assert block_price > 0, "price_w() call failed" + + return block_price + + +@internal +def _check_price_deviation(old_price: uint256, new_price: uint256): + """ + @notice Ensures price returned by new oracle is within acceptable bounds + """ + delta: uint256 = new_price - old_price if old_price < new_price else old_price - new_price + max_delta: uint256 = old_price * self.max_deviation // 10_000 + assert delta <= max_delta, "Price deviation too high" + + +@external +def set_price_oracle(_new_oracle: IPriceOracle, _skip_price_deviation_check: bool = False): + """ + @notice Sets the new oracle contract + @param _new_oracle The new oracle contract + """ + assert msg.sender == self.factory.address, "Not authorized" + + new_price: uint256 = self._validate_price_oracle(_new_oracle) + if not _skip_price_deviation_check: + self._check_price_deviation(staticcall self.oracle.price(), new_price) + + self.oracle = _new_oracle + + log PriceOracleSet(oracle=_new_oracle.address) + + +@external +def set_max_deviation(_max_deviation: uint256): + """ + @notice Allows factory admin to update max price deviation in BPS (e.g. 500 = 5%) + @param _max_deviation New maximum deviation, must be > 0 and <= MAX_DEVIATION_BPS + """ + assert msg.sender == staticcall self.factory.owner(), "Not authorized" + assert _max_deviation > 0 and _max_deviation <= MAX_DEVIATION_BPS, "Invalid deviation" + + self.max_deviation = _max_deviation + + log MaxDeviationSet(max_deviation=_max_deviation) + + +@external +@view +def price() -> uint256: + """ + @notice Passes price() from oracle contract + """ + return staticcall self.oracle.price() + + +@external +def price_w() -> uint256: + """ + @notice Calls price_w() on oracle contract + """ + return extcall self.oracle.price_w() diff --git a/contracts/price_oracles/proxy/ProxyOracleFactory.vy b/contracts/price_oracles/proxy/ProxyOracleFactory.vy new file mode 100644 index 00000000..126f076d --- /dev/null +++ b/contracts/price_oracles/proxy/ProxyOracleFactory.vy @@ -0,0 +1,73 @@ +# @version 0.4.3 +#pragma optimize gas +#pragma evm-version shanghai + +""" +@title ProxyOracleFactory +@author Curve +@license GNU Affero General Public License v3.0 only +@notice Proxy oracle factory deploying proxy oracles and replacing price oracle contracts after the deployment +""" + + +from snekmate.auth import ownable +initializes: ownable +exports: ownable.__interface__ + + +interface IProxyOracle: + def initialize(_oracle: address, _max_deviation: uint256): nonpayable + def set_price_oracle(_new_oracle: address, _skip_price_deviation_check: bool): nonpayable + def oracle() -> address: view + + +event SetOracle: + proxy: indexed(address) + oracle: indexed(address) + + +PROXY_ORACLE_IMPLEMENTATION: public(immutable(address)) +get_proxy: public(HashMap[address, address]) # get_proxy[oracle] -> proxy + + +@deploy +def __init__(admin: address, _proxy_oracle_implementation: address): + """ + @notice Factory which creates StablePool and CryptoPool LP Oracles from blueprints + @param admin Admin of the factory (ideally DAO) + """ + ownable.__init__() + ownable._transfer_ownership(admin) + assert _proxy_oracle_implementation != empty(address) + PROXY_ORACLE_IMPLEMENTATION = _proxy_oracle_implementation + + +@external +@nonreentrant +def deploy_proxy_oracle(_oracle: address) -> address: + """ + @notice Deploy a new LP oracle + @param _oracle Underlying Oracle + @return Deployed oracle address + """ + assert self.get_proxy[_oracle] == empty(address), "Oracle already exists" + + proxy: IProxyOracle = IProxyOracle(create_minimal_proxy_to(PROXY_ORACLE_IMPLEMENTATION)) + extcall proxy.initialize(_oracle, convert(100, uint256)) + self.get_proxy[_oracle] = proxy.address + + log SetOracle(proxy=proxy.address, oracle=_oracle) + + return proxy.address + + +@external +@nonreentrant +def replace_oracle(_proxy: IProxyOracle, _new_oracle: address, _skip_price_deviation_check: bool = False): + ownable._check_owner() + old_oracle: address = staticcall _proxy.oracle() + extcall _proxy.set_price_oracle(_new_oracle, _skip_price_deviation_check) + self.get_proxy[old_oracle] = empty(address) + self.get_proxy[_new_oracle] = _proxy.address + + log SetOracle(proxy=_proxy.address, oracle=_new_oracle) diff --git a/contracts/testing/BlockCounter.vy b/contracts/testing/BlockCounter.vy index 44dac60c..0c0df92b 100644 --- a/contracts/testing/BlockCounter.vy +++ b/contracts/testing/BlockCounter.vy @@ -1,10 +1,10 @@ -# @version 0.3.10 +# version 0.4.3 block_counter: public(uint256) time_counter: public(uint256) last_time: public(uint256) -@external +@deploy def __init__(): self.block_counter = 1 self.time_counter = 0 diff --git a/contracts/testing/ChainlinkAggregatorMock.vy b/contracts/testing/ChainlinkAggregatorMock.vy index beca2619..562af7b3 100644 --- a/contracts/testing/ChainlinkAggregatorMock.vy +++ b/contracts/testing/ChainlinkAggregatorMock.vy @@ -1,4 +1,4 @@ -# @version ^0.3.9 +# pragma version 0.4.3 """ @notice Chainlink Aggregator Mock for testing """ @@ -8,8 +8,7 @@ ADMIN: immutable(address) price: int256 -@payable -@external +@deploy def __init__(decimals: uint8, admin: address, price: int256): self.decimals = decimals diff --git a/contracts/testing/ConstantMonetaryPolicy.vy b/contracts/testing/ConstantMonetaryPolicy.vy index 328cb76e..6703bc58 100644 --- a/contracts/testing/ConstantMonetaryPolicy.vy +++ b/contracts/testing/ConstantMonetaryPolicy.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# @version 0.4.3 """ Although this monetary policy works, it's only intended to be used in tests """ @@ -7,7 +7,7 @@ admin: public(address) rate: public(uint256) -@external +@deploy def __init__(admin: address): self.admin = admin diff --git a/contracts/testing/ConstantMonetaryPolicyLending.vy b/contracts/testing/ConstantMonetaryPolicyLending.vy new file mode 100644 index 00000000..3911fcdc --- /dev/null +++ b/contracts/testing/ConstantMonetaryPolicyLending.vy @@ -0,0 +1,27 @@ +# pragma version 0.4.3 + +from curve_std.interfaces import IERC20 + +_rate: uint256 + +@deploy +def __init__(borrowed_token: IERC20, min_rate: uint256, max_rate: uint256): + # Testing policy: no admin mechanics; initialize to provided min_rate + self._rate = min_rate + + +@external +def rate_write(_for: address = msg.sender) -> uint256: + return self._rate + + +@external +def set_rate(rate: uint256): + # Testing policy: callable by anyone in tests + self._rate = rate + + +@external +@view +def rate(_for: address = msg.sender) -> uint256: + return self._rate diff --git a/contracts/testing/CryptoWithStablePriceWsteth.vy b/contracts/testing/CryptoWithStablePriceWsteth.vy index 45cb661d..c1e06d13 100644 --- a/contracts/testing/CryptoWithStablePriceWsteth.vy +++ b/contracts/testing/CryptoWithStablePriceWsteth.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 """ @title CryptoWithStablePriceWsteth @notice Price oracle for tricrypto+wsteth for crvUSD. Not limiting the price with chainlink - relying on tricrypto-ng @@ -60,7 +60,7 @@ WSTETH: public(immutable(wstETH)) use_chainlink: public(bool) -@external +@deploy def __init__( tricrypto: Tricrypto, ix: uint256, @@ -80,10 +80,10 @@ def __init__( STAKEDSWAP = staked_swap FACTORY = factory WSTETH = wsteth - _stablecoin: address = stable_aggregator.stablecoin() + _stablecoin: address = staticcall stable_aggregator.stablecoin() _redeemable: address = empty(address) STABLECOIN = _stablecoin - coins: address[2] = [stableswap.coins(0), stableswap.coins(1)] + coins: address[2] = [staticcall stableswap.coins(0), staticcall stableswap.coins(1)] is_inverse: bool = False if coins[0] == _stablecoin: _redeemable = coins[1] @@ -93,13 +93,13 @@ def __init__( assert coins[1] == _stablecoin IS_INVERSE = is_inverse REDEEMABLE = _redeemable - assert tricrypto.coins(0) == _redeemable + assert staticcall tricrypto.coins(0) == _redeemable self.use_chainlink = True CHAINLINK_AGGREGATOR_ETH = chainlink_aggregator_eth - CHAINLINK_PRICE_PRECISION_ETH = 10**convert(chainlink_aggregator_eth.decimals(), uint256) + CHAINLINK_PRICE_PRECISION_ETH = 10**convert(staticcall chainlink_aggregator_eth.decimals(), uint256) CHAINLINK_AGGREGATOR_STETH = chainlink_aggregator_steth - CHAINLINK_PRICE_PRECISION_STETH = 10**convert(chainlink_aggregator_steth.decimals(), uint256) + CHAINLINK_PRICE_PRECISION_STETH = 10**convert(staticcall chainlink_aggregator_steth.decimals(), uint256) BOUND_SIZE = bound_size @@ -142,38 +142,38 @@ def redeemable() -> address: @internal @view def _raw_price() -> uint256: - p_crypto_r: uint256 = TRICRYPTO.price_oracle(TRICRYPTO_IX) # d_usdt/d_eth - p_stable_r: uint256 = STABLESWAP.price_oracle() # d_usdt/d_st - p_stable_agg: uint256 = STABLESWAP_AGGREGATOR.price() # d_usd/d_st + p_crypto_r: uint256 = staticcall TRICRYPTO.price_oracle(TRICRYPTO_IX) # d_usdt/d_eth + p_stable_r: uint256 = staticcall STABLESWAP.price_oracle() # d_usdt/d_st + p_stable_agg: uint256 = staticcall STABLESWAP_AGGREGATOR.price() # d_usd/d_st if IS_INVERSE: - p_stable_r = 10**36 / p_stable_r - crv_p: uint256 = p_crypto_r * p_stable_agg / p_stable_r # d_usd/d_eth + p_stable_r = 10**36 // p_stable_r + crv_p: uint256 = p_crypto_r * p_stable_agg // p_stable_r # d_usd/d_eth use_chainlink: bool = self.use_chainlink # Limit ETH price if use_chainlink: - chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData() + chainlink_lrd: ChainlinkAnswer = staticcall CHAINLINK_AGGREGATOR_ETH.latestRoundData() if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD: - chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH - lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18 - upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18 + chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 // CHAINLINK_PRICE_PRECISION_ETH + lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) // 10**18 + upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) // 10**18 crv_p = min(max(crv_p, lower), upper) - p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth + p_staked: uint256 = staticcall STAKEDSWAP.price_oracle() # d_eth / d_steth # Limit STETH price if use_chainlink: - chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData() + chainlink_lrd: ChainlinkAnswer = staticcall CHAINLINK_AGGREGATOR_STETH.latestRoundData() if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD: - chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH - lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18 - upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18 + chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 // CHAINLINK_PRICE_PRECISION_STETH + lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) // 10**18 + upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) // 10**18 p_staked = min(max(p_staked, lower), upper) - p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth + p_staked = min(p_staked, 10**18) * staticcall WSTETH.stEthPerToken() // 10**18 # d_eth / d_wsteth - return p_staked * crv_p / 10**18 + return p_staked * crv_p // 10**18 @external @@ -195,5 +195,5 @@ def price_w() -> uint256: @external def set_use_chainlink(do_it: bool): - assert msg.sender == FACTORY.admin() + assert msg.sender == staticcall FACTORY.admin() self.use_chainlink = do_it diff --git a/contracts/testing/DummyCallback.vy b/contracts/testing/DummyCallback.vy new file mode 100644 index 00000000..0817a1cd --- /dev/null +++ b/contracts/testing/DummyCallback.vy @@ -0,0 +1,47 @@ +# pragma version 0.4.3 +from curve_std import token as tkn +from curve_std.interfaces import IERC20 +from contracts import constants as c + +callback_deposit_hits: public(uint256) +callback_repay_hits: public(uint256) +callback_liquidate_hits: public(uint256) + + +@internal +def _callback(calldata: Bytes[c.CALLDATA_MAX_SIZE]) -> uint256[2]: + borrowed_token: IERC20 = empty(IERC20) + collateral_token: IERC20 = empty(IERC20) + borrowed_amount: uint256 = 0 + collateral_amount: uint256 = 0 + borrowed_token, collateral_token, borrowed_amount, collateral_amount = \ + abi_decode(calldata, (IERC20, IERC20, uint256, uint256,)) + + tkn.max_approve(borrowed_token, msg.sender) + tkn.max_approve(collateral_token, msg.sender) + + assert staticcall borrowed_token.balanceOf(self) >= borrowed_amount, "Not enough borrowed tokens in callback contract" + assert staticcall collateral_token.balanceOf(self) >= collateral_amount, "Not enough collaterals token in callback contract" + + return [borrowed_amount, collateral_amount] + + +@external +def callback_deposit(user: address, borrowed: uint256, collateral: uint256, + debt: uint256, calldata: Bytes[c.CALLDATA_MAX_SIZE]) -> uint256[2]: + self.callback_deposit_hits += 1 + return self._callback(calldata) + + +@external +def callback_repay(user: address, borrowed: uint256, collateral: uint256, + debt: uint256, calldata: Bytes[c.CALLDATA_MAX_SIZE]) -> uint256[2]: + self.callback_repay_hits += 1 + return self._callback(calldata) + + +@external +def callback_liquidate(sender: address, stablecoins: uint256, collateral: uint256, + debt: uint256, calldata: Bytes[c.CALLDATA_MAX_SIZE]) -> uint256[2]: + self.callback_liquidate_hits += 1 + return self._callback(calldata) diff --git a/contracts/testing/DummyFlashBorrower.vy b/contracts/testing/DummyFlashBorrower.vy index ffbdf227..65ec79c8 100644 --- a/contracts/testing/DummyFlashBorrower.vy +++ b/contracts/testing/DummyFlashBorrower.vy @@ -1,6 +1,6 @@ -# @version 0.3.10 +# pragma version 0.4.3 -from vyper.interfaces import ERC20 +from curve_std.interfaces import IERC20 interface ERC3156FlashLender: def flashFee(token: address, amount: uint256) -> uint256: view @@ -15,7 +15,7 @@ success: bool send_back: bool -@external +@deploy def __init__(_lender: address): """ @notice FlashBorrower constructor. Gets FlashLender address. @@ -37,14 +37,14 @@ def onFlashLoan( assert msg.sender == LENDER, "FlashBorrower: Untrusted lender" assert initiator == self, "FlashBorrower: Untrusted loan initiator" assert data == b"", "Non-empty data" - assert ERC20(token).balanceOf(self) == amount + assert staticcall IERC20(token).balanceOf(self) == amount assert fee == 0 self.count += 1 self.total_amount += amount if self.send_back: - ERC20(token).transfer(LENDER, amount + fee) + extcall IERC20(token).transfer(LENDER, amount + fee) return keccak256("ERC3156FlashBorrower.onFlashLoan") @@ -54,4 +54,4 @@ def flashBorrow(token: address, amount: uint256, send_back: bool = True): @notice Initiate a flash loan. """ self.send_back = send_back - ERC3156FlashLender(LENDER).flashLoan(self, token, amount, b"") + extcall ERC3156FlashLender(LENDER).flashLoan(self, token, amount, b"") diff --git a/contracts/testing/DummyLMCallback.vy b/contracts/testing/DummyLMCallback.vy index a68b300f..39f72d21 100644 --- a/contracts/testing/DummyLMCallback.vy +++ b/contracts/testing/DummyLMCallback.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 MAX_TICKS_UINT: constant(uint256) = 50 @@ -8,7 +8,7 @@ _debug_user_shares: public(HashMap[address, HashMap[int256, uint256]]) AMM: immutable(address) -@external +@deploy def __init__(amm: address): AMM = amm @@ -17,7 +17,7 @@ def __init__(amm: address): def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT]): assert msg.sender == AMM i: int256 = n - for s in collateral_per_share: + for s: uint256 in collateral_per_share: self._debug_collateral_per_share[i] = s i += 1 @@ -26,6 +26,6 @@ def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256 def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT]): assert msg.sender == AMM i: int256 = n - for s in user_shares: + for s: uint256 in user_shares: self._debug_user_shares[user][i] = s i += 1 diff --git a/contracts/testing/DummyPriceOracle.vy b/contracts/testing/DummyPriceOracle.vy index 5b4d1d49..895b7d7a 100644 --- a/contracts/testing/DummyPriceOracle.vy +++ b/contracts/testing/DummyPriceOracle.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 """ This contract is for testing only. @@ -9,7 +9,7 @@ price: public(uint256) ADMIN: immutable(address) -@external +@deploy def __init__(admin: address, price: uint256): self.price = price ADMIN = admin diff --git a/contracts/testing/ERC20CRV.vy b/contracts/testing/ERC20CRV.vy index 3b993888..0fcbee8e 100644 --- a/contracts/testing/ERC20CRV.vy +++ b/contracts/testing/ERC20CRV.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# @version 0.4.3 """ @title Curve DAO Token @author Curve Finance @@ -8,9 +8,9 @@ https://eips.ethereum.org/EIPS/eip-20 """ -from vyper.interfaces import ERC20 +from ethereum.ercs import IERC20 -implements: ERC20 +implements: IERC20 event Transfer: @@ -60,7 +60,7 @@ YEAR: constant(uint256) = 86400 * 365 # Supply parameters INITIAL_SUPPLY: constant(uint256) = 1_303_030_303 -INITIAL_RATE: constant(uint256) = 274_815_283 * 10 ** 18 / YEAR # leading to 43% premine +INITIAL_RATE: constant(uint256) = 274_815_283 * 10 ** 18 // YEAR # leading to 43% premine RATE_REDUCTION_TIME: constant(uint256) = YEAR RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024 # 2 ** (1/4) * 1e18 RATE_DENOMINATOR: constant(uint256) = 10 ** 18 @@ -74,7 +74,7 @@ rate: public(uint256) start_epoch_supply: uint256 -@external +@deploy def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): """ @notice Contract constructor @@ -89,7 +89,7 @@ def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): self.balanceOf[msg.sender] = init_supply self.total_supply = init_supply self.admin = msg.sender - log Transfer(ZERO_ADDRESS, msg.sender, init_supply) + log Transfer(_from=empty(address), _to=msg.sender, _value=init_supply) self.start_epoch_time = block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME self.mining_epoch = -1 @@ -114,11 +114,11 @@ def _update_mining_parameters(): else: _start_epoch_supply += _rate * RATE_REDUCTION_TIME self.start_epoch_supply = _start_epoch_supply - _rate = _rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT + _rate = _rate * RATE_DENOMINATOR // RATE_REDUCTION_COEFFICIENT self.rate = _rate - log UpdateMiningParameters(block.timestamp, _rate, _start_epoch_supply) + log UpdateMiningParameters(time=block.timestamp, rate=_rate, supply=_start_epoch_supply) @external @@ -194,11 +194,11 @@ def mintable_in_timeframe(start: uint256, end: uint256) -> uint256: # Special case if end is in future (not yet minted) epoch if end > current_epoch_time + RATE_REDUCTION_TIME: current_epoch_time += RATE_REDUCTION_TIME - current_rate = current_rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT + current_rate = current_rate * RATE_DENOMINATOR // RATE_REDUCTION_COEFFICIENT assert end <= current_epoch_time + RATE_REDUCTION_TIME # dev: too far in future - for i in range(999): # Curve will not work in 1000 years. Darn! + for i: uint256 in range(999): # Curve will not work in 1000 years. Darn! if end >= current_epoch_time: current_end: uint256 = end if current_end > current_epoch_time + RATE_REDUCTION_TIME: @@ -216,7 +216,7 @@ def mintable_in_timeframe(start: uint256, end: uint256) -> uint256: break current_epoch_time -= RATE_REDUCTION_TIME - current_rate = current_rate * RATE_REDUCTION_COEFFICIENT / RATE_DENOMINATOR # double-division with rounding made rate a bit less => good + current_rate = current_rate * RATE_REDUCTION_COEFFICIENT // RATE_DENOMINATOR # double-division with rounding made rate a bit less => good assert current_rate <= INITIAL_RATE # This should never happen return to_mint @@ -230,9 +230,9 @@ def set_minter(_minter: address): @param _minter Address of the minter """ assert msg.sender == self.admin # dev: admin only - assert self.minter == ZERO_ADDRESS # dev: can set the minter only once, at creation + assert self.minter == empty(address) # dev: can set the minter only once, at creation self.minter = _minter - log SetMinter(_minter) + log SetMinter(minter=_minter) @external @@ -244,7 +244,7 @@ def set_admin(_admin: address): """ assert msg.sender == self.admin # dev: admin only self.admin = _admin - log SetAdmin(_admin) + log SetAdmin(admin=_admin) @external @@ -278,10 +278,10 @@ def transfer(_to : address, _value : uint256) -> bool: @param _value The amount to be transferred @return bool success """ - assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed + assert _to != empty(address) # dev: transfers to 0x0 are not allowed self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value - log Transfer(msg.sender, _to, _value) + log Transfer(_from=msg.sender, _to=_to, _value=_value) return True @@ -294,13 +294,13 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: @param _value uint256 the amount of tokens to be transferred @return bool success """ - assert _to != ZERO_ADDRESS # dev: transfers to 0x0 are not allowed + assert _to != empty(address) # dev: transfers to 0x0 are not allowed # NOTE: vyper does not allow underflows # so the following subtraction would revert on insufficient balance self.balanceOf[_from] -= _value self.balanceOf[_to] += _value self.allowances[_from][msg.sender] -= _value - log Transfer(_from, _to, _value) + log Transfer(_from=_from, _to=_to, _value=_value) return True @@ -317,7 +317,7 @@ def approve(_spender : address, _value : uint256) -> bool: """ assert _value == 0 or self.allowances[msg.sender][_spender] == 0 self.allowances[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) + log Approval(_owner=msg.sender, _spender=_spender, _value=_value) return True @@ -331,7 +331,7 @@ def mint(_to: address, _value: uint256) -> bool: @return bool success """ assert msg.sender == self.minter # dev: minter only - assert _to != ZERO_ADDRESS # dev: zero address + assert _to != empty(address) # dev: zero address if block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME: self._update_mining_parameters() @@ -341,7 +341,7 @@ def mint(_to: address, _value: uint256) -> bool: self.total_supply = _total_supply self.balanceOf[_to] += _value - log Transfer(ZERO_ADDRESS, _to, _value) + log Transfer(_from=empty(address), _to=_to, _value=_value) return True @@ -357,7 +357,7 @@ def burn(_value: uint256) -> bool: self.balanceOf[msg.sender] -= _value self.total_supply -= _value - log Transfer(msg.sender, ZERO_ADDRESS, _value) + log Transfer(_from=msg.sender, _to=empty(address), _value=_value) return True diff --git a/contracts/testing/ERC20Mock.vy b/contracts/testing/ERC20Mock.vy index 4070b457..ee795ca4 100644 --- a/contracts/testing/ERC20Mock.vy +++ b/contracts/testing/ERC20Mock.vy @@ -1,73 +1,18 @@ -# @version 0.3.10 -""" -@notice Mock ERC20 for testing -""" +# pragma version 0.4.3 -event Transfer: - _from: indexed(address) - _to: indexed(address) - _value: uint256 +from curve_std.interfaces import IERC20 +implements: IERC20 -event Approval: - _owner: indexed(address) - _spender: indexed(address) - _value: uint256 +from snekmate.tokens import erc20 +from snekmate.auth import ownable -name: public(String[64]) -symbol: public(String[32]) -decimals: public(uint256) -balanceOf: public(HashMap[address, uint256]) -allowances: HashMap[address, HashMap[address, uint256]] -total_supply: uint256 +initializes: ownable +initializes: erc20[ownable := ownable] +exports: erc20.__interface__ -@external -def __init__(_name: String[64], _symbol: String[32], _decimals: uint256): - self.name = _name - self.symbol = _symbol - self.decimals = _decimals - -@external -@view -def totalSupply() -> uint256: - return self.total_supply - - -@external -@view -def allowance(_owner : address, _spender : address) -> uint256: - return self.allowances[_owner][_spender] - - -@external -def transfer(_to : address, _value : uint256) -> bool: - self.balanceOf[msg.sender] -= _value - self.balanceOf[_to] += _value - log Transfer(msg.sender, _to, _value) - return True - - -@external -def transferFrom(_from : address, _to : address, _value : uint256) -> bool: - self.balanceOf[_from] -= _value - self.balanceOf[_to] += _value - self.allowances[_from][msg.sender] -= _value - log Transfer(_from, _to, _value) - return True - - -@external -def approve(_spender : address, _value : uint256) -> bool: - self.allowances[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) - return True - - -@external -def _mint_for_testing(_target: address, _value: uint256) -> bool: - self.total_supply += _value - self.balanceOf[_target] += _value - log Transfer(ZERO_ADDRESS, _target, _value) - - return True +@deploy +def __init__(decimals: uint256): + ownable.__init__() + erc20.__init__("mock", "mock", convert(decimals, uint8), "mock", "mock") diff --git a/contracts/testing/FakeLeverage.vy b/contracts/testing/FakeLeverage.vy index 3fc9af5f..206d7a42 100644 --- a/contracts/testing/FakeLeverage.vy +++ b/contracts/testing/FakeLeverage.vy @@ -1,21 +1,23 @@ -# @version 0.3.10 -from vyper.interfaces import ERC20 +# pragma version 0.4.3 +from curve_std.interfaces import IERC20 -STABLECOIN: immutable(ERC20) -COLLATERAL: immutable(ERC20) +STABLECOIN: immutable(IERC20) +COLLATERAL: immutable(IERC20) price: public(uint256) +callback_deposit_hits: public(uint256) +callback_repay_hits: public(uint256) -@external -def __init__(stablecoin_token: ERC20, collateral_token: ERC20, controller: address, price: uint256): +@deploy +def __init__(stablecoin_token: IERC20, collateral_token: IERC20, controller: address, price: uint256): STABLECOIN = stablecoin_token COLLATERAL = collateral_token self.price = price # It is necessary to approve transfers of these tokens by the controller - stablecoin_token.approve(controller, max_value(uint256)) - collateral_token.approve(controller, max_value(uint256)) + extcall stablecoin_token.approve(controller, max_value(uint256)) + extcall collateral_token.approve(controller, max_value(uint256)) # This contract will just receive funding in tokens and "swap" them according to the price @@ -23,28 +25,30 @@ def __init__(stablecoin_token: ERC20, collateral_token: ERC20, controller: addre @external def approve_all(): # Don't do this at home - only for tests! - STABLECOIN.approve(msg.sender, max_value(uint256)) - COLLATERAL.approve(msg.sender, max_value(uint256)) + extcall STABLECOIN.approve(msg.sender, max_value(uint256)) + extcall COLLATERAL.approve(msg.sender, max_value(uint256)) @external -def callback_deposit(user: address, stablecoins_no_use: uint256, collateral: uint256, debt: uint256, extra_args: DynArray[uint256, 5]) -> uint256[2]: - min_amount: uint256 = extra_args[0] - assert STABLECOIN.balanceOf(self) >= debt - amount_out: uint256 = debt * 10**18 / self.price +def callback_deposit(user: address, stablecoins_no_use: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: + self.callback_deposit_hits += 1 + min_amount: uint256 = abi_decode(calldata, (uint256)) + assert staticcall STABLECOIN.balanceOf(self) >= debt + amount_out: uint256 = debt * 10**18 // self.price assert amount_out >= min_amount return [0, amount_out] @external -def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, extra_args: DynArray[uint256, 5]) -> uint256[2]: - frac: uint256 = extra_args[0] - s_diff: uint256 = (debt - stablecoins) * frac / 10**18 +def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: + self.callback_repay_hits += 1 + frac: uint256 = abi_decode(calldata, (uint256)) + s_diff: uint256 = (debt - stablecoins) * frac // 10**18 # Instead of returning collateral - what_was_spent we could unwrap and send # ETH from here to user (if it was ETH), so no need to do it in controller - return [s_diff, collateral - s_diff * 10**18 / self.price] + return [s_diff, collateral - s_diff * 10**18 // self.price] @external -def callback_liquidate(sender: address, stablecoins: uint256, collateral: uint256, debt: uint256, extra_args: DynArray[uint256, 5]) -> uint256[2]: - return [STABLECOIN.balanceOf(self), collateral] +def callback_liquidate(sender: address, stablecoins: uint256, collateral: uint256, debt: uint256, calldata: Bytes[10**4]) -> uint256[2]: + return [staticcall STABLECOIN.balanceOf(self), collateral] diff --git a/contracts/testing/MockFactory.vy b/contracts/testing/MockFactory.vy index 9825bb4f..720c16b0 100644 --- a/contracts/testing/MockFactory.vy +++ b/contracts/testing/MockFactory.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 interface Controller: @@ -21,17 +21,17 @@ def add_market(controller: address, ceiling: uint256): @external def set_debt(controller: address, debt: uint256): - Controller(controller).set_debt(debt) + extcall Controller(controller).set_debt(debt) @external @view def total_debt() -> uint256: total: uint256 = 0 - for i in range(10000): + for i: uint256 in range(10000): if i == self.n_collaterals: break - total += Controller(self.controllers[i]).total_debt() + total += staticcall Controller(self.controllers[i]).total_debt() return total diff --git a/contracts/testing/MockMarket.vy b/contracts/testing/MockMarket.vy index 6d375c1f..b19e3c37 100644 --- a/contracts/testing/MockMarket.vy +++ b/contracts/testing/MockMarket.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 total_debt: public(uint256) diff --git a/contracts/testing/MockPegKeeper.vy b/contracts/testing/MockPegKeeper.vy index 5d9960d7..a890e06a 100644 --- a/contracts/testing/MockPegKeeper.vy +++ b/contracts/testing/MockPegKeeper.vy @@ -1,4 +1,4 @@ -# @version 0.3.10 +# pragma version 0.4.3 """ This contract is for testing only. @@ -16,7 +16,7 @@ totalSupply: public(uint256) price: public(uint256) -@external +@deploy def __init__(price: uint256, stablecoin: address): pool = self self.IS_INVERSE = False diff --git a/contracts/testing/MockRateOracle.vy b/contracts/testing/MockRateOracle.vy index 39fed5a1..3d7fdf35 100644 --- a/contracts/testing/MockRateOracle.vy +++ b/contracts/testing/MockRateOracle.vy @@ -1,4 +1,4 @@ -#pragma version 0.3.10 +# pragma version 0.4.3 """ @title MockRateOracle @notice Mock to tweak rate @@ -7,7 +7,7 @@ rates: public(uint256[2]) -@external +@deploy def __init__(): self.rates = [10 ** 18, 10 ** 18] diff --git a/contracts/testing/OldAMM.vy b/contracts/testing/OldAMM.vy deleted file mode 100644 index 28016335..00000000 --- a/contracts/testing/OldAMM.vy +++ /dev/null @@ -1,1731 +0,0 @@ -# @version 0.3.10 -""" -@title LLAMMA - crvUSD AMM -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -""" - -# Glossary of variables and terms -# ======================= -# * ticks, bands - price ranges where liquidity is deposited -# * x - coin which is being borrowed, typically stablecoin -# * y - collateral coin (for example, wETH) -# * A - amplification, the measure of how concentrated the tick is -# * rate - interest rate -# * rate_mul - rate multiplier, 1 + integral(rate * dt) -# * active_band - current band. Other bands are either in one or other coin, but not both -# * min_band - bands below this are definitely empty -# * max_band - bands above this are definitely empty -# * bands_x[n], bands_y[n] - amounts of coin x or y deposited in band n -# * user_shares[user,n] / total_shares[n] - fraction of n'th band owned by a user -# * p_oracle - external oracle price (can be from another AMM) -# * p (as in get_p) - current price of AMM. It depends not only on the balances (x,y) in the band and active_band, but -# also on p_oracle -# * p_current_up, p_current_down - the value of p at constant p_oracle when y=0 or x=0 respectively for the band n -# * p_oracle_up, p_oracle_down - edges of the band when p=p_oracle (steady state), happen when x=0 or y=0 respectively, -# for band n. -# * Grid of bands is set for p_oracle values such as: -# * p_oracle_up(n) = base_price * ((A - 1) / A)**n -# * p_oracle_down(n) = p_oracle_up(n) * (A - 1) / A = p_oracle_up(n+1) -# * p_current_up and p_oracle_up change in opposite directions with n -# * When intereste is accrued - all the grid moves by change of base_price -# -# Bonding curve reads as: -# (f + x) * (g + y) = Inv = p_oracle * A**2 * y0**2 -# ======================= - -interface ERC20: - def transfer(_to: address, _value: uint256) -> bool: nonpayable - def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable - def approve(_spender: address, _value: uint256) -> bool: nonpayable - -interface PriceOracle: - def price() -> uint256: view - def price_w() -> uint256: nonpayable - -interface LMGauge: - def callback_collateral_shares(n: int256, collateral_per_share: DynArray[uint256, MAX_TICKS_UINT]): nonpayable - def callback_user_shares(user: address, n: int256, user_shares: DynArray[uint256, MAX_TICKS_UINT]): nonpayable - - -event TokenExchange: - buyer: indexed(address) - sold_id: uint256 - tokens_sold: uint256 - bought_id: uint256 - tokens_bought: uint256 - -event Deposit: - provider: indexed(address) - amount: uint256 - n1: int256 - n2: int256 - -event Withdraw: - provider: indexed(address) - amount_borrowed: uint256 - amount_collateral: uint256 - -event SetRate: - rate: uint256 - rate_mul: uint256 - time: uint256 - -event SetFee: - fee: uint256 - -event SetAdminFee: - fee: uint256 - - -MAX_TICKS: constant(int256) = 50 -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_SKIP_TICKS: constant(int256) = 1024 - - -struct UserTicks: - ns: int256 # packs n1 and n2, each is int128 - ticks: uint256[MAX_TICKS/2] # Share fractions packed 2 per slot - -struct DetailedTrade: - in_amount: uint256 - out_amount: uint256 - n1: int256 - n2: int256 - ticks_in: DynArray[uint256, MAX_TICKS_UINT] - last_tick_j: uint256 - admin_fee: uint256 - - -BORROWED_TOKEN: immutable(ERC20) # x -BORROWED_PRECISION: immutable(uint256) -COLLATERAL_TOKEN: immutable(ERC20) # y -COLLATERAL_PRECISION: immutable(uint256) -BASE_PRICE: immutable(uint256) -admin: public(address) - -A: public(immutable(uint256)) -Aminus1: immutable(uint256) -A2: immutable(uint256) -Aminus12: immutable(uint256) -SQRT_BAND_RATIO: immutable(uint256) # sqrt(A / (A - 1)) -LOG_A_RATIO: immutable(int256) # ln(A / (A - 1)) -MAX_ORACLE_DN_POW: immutable(uint256) # (A / (A - 1)) ** 50 - -fee: public(uint256) -admin_fee: public(uint256) -rate: public(uint256) -rate_time: uint256 -rate_mul: uint256 -active_band: public(int256) -min_band: public(int256) -max_band: public(int256) - -admin_fees_x: public(uint256) -admin_fees_y: public(uint256) - -price_oracle_contract: public(immutable(PriceOracle)) -old_p_o: uint256 -old_dfee: uint256 -prev_p_o_time: uint256 -PREV_P_O_DELAY: constant(uint256) = 2 * 60 # s = 2 min -MAX_P_O_CHG: constant(uint256) = 12500 * 10**14 # <= 2**(1/3) - max relative change to have fee < 50% - -bands_x: public(HashMap[int256, uint256]) -bands_y: public(HashMap[int256, uint256]) - -total_shares: HashMap[int256, uint256] -user_shares: HashMap[address, UserTicks] -DEAD_SHARES: constant(uint256) = 1000 - -liquidity_mining_callback: public(LMGauge) - - -@external -def __init__( - _borrowed_token: address, - _borrowed_precision: uint256, - _collateral_token: address, - _collateral_precision: uint256, - _A: uint256, - _sqrt_band_ratio: uint256, - _log_A_ratio: int256, - _base_price: uint256, - fee: uint256, - admin_fee: uint256, - _price_oracle_contract: address, - ): - """ - @notice LLAMMA constructor - @param _borrowed_token Token which is being borrowed - @param _collateral_token Token used as collateral - @param _collateral_precision Precision of collateral: we pass it because we want the blueprint to fit into bytecode - @param _A "Amplification coefficient" which also defines density of liquidity and band size. Relative band size is 1/_A - @param _sqrt_band_ratio Precomputed int(sqrt(A / (A - 1)) * 1e18) - @param _log_A_ratio Precomputed int(ln(A / (A - 1)) * 1e18) - @param _base_price Typically the initial crypto price at which AMM is deployed. Will correspond to band 0 - @param fee Relative fee of the AMM: int(fee * 1e18) - @param admin_fee Admin fee: how much of fee goes to admin. 50% === int(0.5 * 1e18) - @param _price_oracle_contract External price oracle which has price() and price_w() methods - which both return current price of collateral multiplied by 1e18 - """ - BORROWED_TOKEN = ERC20(_borrowed_token) - BORROWED_PRECISION = _borrowed_precision - COLLATERAL_TOKEN = ERC20(_collateral_token) - COLLATERAL_PRECISION = _collateral_precision - A = _A - BASE_PRICE = _base_price - - Aminus1 = unsafe_sub(A, 1) - A2 = pow_mod256(A, 2) - Aminus12 = pow_mod256(unsafe_sub(A, 1), 2) - - self.fee = fee - self.admin_fee = admin_fee - price_oracle_contract = PriceOracle(_price_oracle_contract) - self.prev_p_o_time = block.timestamp - self.old_p_o = price_oracle_contract.price() - - self.rate_mul = 10**18 - - # sqrt(A / (A - 1)) - needs to be pre-calculated externally - SQRT_BAND_RATIO = _sqrt_band_ratio - # log(A / (A - 1)) - needs to be pre-calculated externally - LOG_A_RATIO = _log_A_ratio - - # (A / (A - 1)) ** 50 - # This is not gas-optimal but good with bytecode size and does not overflow - pow: uint256 = 10**18 - for i in range(50): - pow = unsafe_div(pow * A, Aminus1) - MAX_ORACLE_DN_POW = pow - - -@internal -def approve_max(token: ERC20, _admin: address): - """ - Approve max in a separate function because it uses less bytespace than - calling directly, and gas doesn't matter in set_admin - """ - assert token.approve(_admin, max_value(uint256), default_return_value=True) - - -@external -def set_admin(_admin: address): - """ - @notice Set admin of the AMM. Typically it's a controller (unless it's tests) - @param _admin Admin address - """ - assert self.admin == empty(address) - self.admin = _admin - self.approve_max(BORROWED_TOKEN, _admin) - self.approve_max(COLLATERAL_TOKEN, _admin) - - -@internal -@pure -def sqrt_int(_x: uint256) -> uint256: - """ - @notice Wrapping isqrt builtin because otherwise it will be repeated every time instead of calling - @param _x Square root's input in "normal" units, e.g. sqrt_int(1) == 1 - """ - return isqrt(_x) - - -@external -@pure -def coins(i: uint256) -> address: - return [BORROWED_TOKEN.address, COLLATERAL_TOKEN.address][i] - - -@internal -@view -def limit_p_o(p: uint256) -> uint256[2]: - """ - @notice Limits oracle price to avoid losses at abrupt changes, as well as calculates a dynamic fee. - If we consider oracle_change such as: - ratio = p_new / p_old - (let's take for simplicity p_new < p_old, otherwise we compute p_old / p_new) - Then if the minimal AMM fee will be: - fee = (1 - ratio**3), - AMM will not have a loss associated with the price change. - However, over time fee should still go down (over PREV_P_O_DELAY), and also ratio should be limited - because we don't want the fee to become too large (say, 50%) which is achieved by limiting the instantaneous - change in oracle price. - - @return (limited_price_oracle, dynamic_fee) - """ - p_new: uint256 = p - dt: uint256 = unsafe_sub(PREV_P_O_DELAY, min(PREV_P_O_DELAY, block.timestamp - self.prev_p_o_time)) - ratio: uint256 = 0 - - # ratio = 1 - (p_o_min / p_o_max)**3 - - if dt > 0: - old_p_o: uint256 = self.old_p_o - old_ratio: uint256 = self.old_dfee - # ratio = p_o_min / p_o_max - if p > old_p_o: - ratio = unsafe_div(old_p_o * 10**18, p) - if ratio < 10**36 / MAX_P_O_CHG: - p_new = unsafe_div(old_p_o * MAX_P_O_CHG, 10**18) - ratio = 10**36 / MAX_P_O_CHG - else: - ratio = unsafe_div(p * 10**18, old_p_o) - if ratio < 10**36 / MAX_P_O_CHG: - p_new = unsafe_div(old_p_o * 10**18, MAX_P_O_CHG) - ratio = 10**36 / MAX_P_O_CHG - - # ratio is lower than 1e18 - # Also guaranteed to be limited, therefore can have all ops unsafe - ratio = min( - unsafe_div( - unsafe_mul( - unsafe_sub(unsafe_add(10**18, old_ratio), unsafe_div(pow_mod256(ratio, 3), 10**36)), # (f' + (1 - r**3)) - dt), # * dt / T - PREV_P_O_DELAY), - 10**18 - 1) - - return [p_new, ratio] - - -@internal -@view -def _price_oracle_ro() -> uint256[2]: - return self.limit_p_o(price_oracle_contract.price()) - - -@internal -def _price_oracle_w() -> uint256[2]: - p: uint256[2] = self.limit_p_o(price_oracle_contract.price_w()) - self.prev_p_o_time = block.timestamp - self.old_p_o = p[0] - self.old_dfee = p[1] - return p - - -@external -@view -def price_oracle() -> uint256: - """ - @notice Value returned by the external price oracle contract - """ - return self._price_oracle_ro()[0] - - -@external -@view -def dynamic_fee() -> uint256: - """ - @notice Dynamic fee which accounts for price_oracle shifts - """ - return max(self.fee, self._price_oracle_ro()[1]) - - -@internal -@view -def _rate_mul() -> uint256: - """ - @notice Rate multiplier which is 1.0 + integral(rate, dt) - @return Rate multiplier in units where 1.0 == 1e18 - """ - return unsafe_div(self.rate_mul * (10**18 + self.rate * (block.timestamp - self.rate_time)), 10**18) - - -@external -@view -def get_rate_mul() -> uint256: - """ - @notice Rate multiplier which is 1.0 + integral(rate, dt) - @return Rate multiplier in units where 1.0 == 1e18 - """ - return self._rate_mul() - - -@internal -@view -def _base_price() -> uint256: - """ - @notice Price which corresponds to band 0. - Base price grows with time to account for interest rate (which is 0 by default) - """ - return unsafe_div(BASE_PRICE * self._rate_mul(), 10**18) - - -@external -@view -def get_base_price() -> uint256: - """ - @notice Price which corresponds to band 0. - Base price grows with time to account for interest rate (which is 0 by default) - """ - return self._base_price() - - -@internal -@view -def _p_oracle_up(n: int256) -> uint256: - """ - @notice Upper oracle price for the band to have liquidity when p = p_oracle - @param n Band number (can be negative) - @return Price at 1e18 base - """ - # p_oracle_up(n) = p_base * ((A - 1) / A) ** n - # p_oracle_down(n) = p_base * ((A - 1) / A) ** (n + 1) = p_oracle_up(n+1) - # return unsafe_div(self._base_price() * self.exp_int(-n * LOG_A_RATIO), 10**18) - - power: int256 = -n * LOG_A_RATIO - - # ((A - 1) / A) ** n = exp(-n * ln(A / (A - 1))) = exp(-n * LOG_A_RATIO) - ## Exp implementation based on solmate's - assert power > -41446531673892821376 - assert power < 135305999368893231589 - - x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18) - - k: int256 = unsafe_div( - unsafe_add( - unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128), - 2**95), - 2**96) - x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128)) - - y: int256 = unsafe_add(x, 1346386616545796478920950773328) - y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442) - p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812) - p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240) - p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96)) - - q: int256 = x - 2855989394907223263936484059900 - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023) - - exp_result: uint256 = shift( - unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667), - unsafe_sub(k, 195)) - ## End exp - assert exp_result > 1000 # dev: limit precision of the multiplier - return unsafe_div(self._base_price() * exp_result, 10**18) - - -@internal -@view -def _p_current_band(n: int256) -> uint256: - """ - @notice Lowest possible price of the band at current oracle price - @param n Band number (can be negative) - @return Price at 1e18 base - """ - # k = (self.A - 1) / self.A # equal to (p_down / p_up) - # p_base = self.p_base * k ** n = p_oracle_up(n) - p_base: uint256 = self._p_oracle_up(n) - - # return self.p_oracle**3 / p_base**2 - p_oracle: uint256 = self._price_oracle_ro()[0] - return unsafe_div(p_oracle**2 / p_base * p_oracle, p_base) - - -@external -@view -def p_current_up(n: int256) -> uint256: - """ - @notice Highest possible price of the band at current oracle price - @param n Band number (can be negative) - @return Price at 1e18 base - """ - return self._p_current_band(n + 1) - - -@external -@view -def p_current_down(n: int256) -> uint256: - """ - @notice Lowest possible price of the band at current oracle price - @param n Band number (can be negative) - @return Price at 1e18 base - """ - return self._p_current_band(n) - - -@external -@view -def p_oracle_up(n: int256) -> uint256: - """ - @notice Highest oracle price for the band to have liquidity when p = p_oracle - @param n Band number (can be negative) - @return Price at 1e18 base - """ - return self._p_oracle_up(n) - - -@external -@view -def p_oracle_down(n: int256) -> uint256: - """ - @notice Lowest oracle price for the band to have liquidity when p = p_oracle - @param n Band number (can be negative) - @return Price at 1e18 base - """ - return self._p_oracle_up(n + 1) - - -@internal -@pure -def _get_y0(x: uint256, y: uint256, p_o: uint256, p_o_up: uint256) -> uint256: - """ - @notice Calculate y0 for the invariant based on current liquidity in band. - The value of y0 has a meaning of amount of collateral when band has no stablecoin - but current price is equal to both oracle price and upper band price. - @param x Amount of stablecoin in band - @param y Amount of collateral in band - @param p_o External oracle price - @param p_o_up Upper boundary of the band - @return y0 - """ - assert p_o != 0 - # solve: - # p_o * A * y0**2 - y0 * (p_oracle_up/p_o * (A-1) * x + p_o**2/p_oracle_up * A * y) - xy = 0 - b: uint256 = 0 - # p_o_up * unsafe_sub(A, 1) * x / p_o + A * p_o**2 / p_o_up * y / 10**18 - if x != 0: - b = unsafe_div(p_o_up * Aminus1 * x, p_o) - if y != 0: - b += unsafe_div(A * p_o**2 / p_o_up * y, 10**18) - if x > 0 and y > 0: - D: uint256 = b**2 + unsafe_div((unsafe_mul(4, A) * p_o) * y, 10**18) * x - return unsafe_div((b + self.sqrt_int(D)) * 10**18, unsafe_mul(unsafe_mul(2, A), p_o)) - else: - return unsafe_div(b * 10**18, unsafe_mul(A, p_o)) - - -@internal -@view -def _get_p(n: int256, x: uint256, y: uint256) -> uint256: - """ - @notice Get current AMM price in band - @param n Band number - @param x Amount of stablecoin in band - @param y Amount of collateral in band - @return Current price at 1e18 base - """ - p_o_up: uint256 = self._p_oracle_up(n) - p_o: uint256 = self._price_oracle_ro()[0] - assert p_o_up != 0 - - # Special cases - if x == 0: - if y == 0: # x and y are 0 - # Return mid-band - return unsafe_div((unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) * A), Aminus1) - # if x == 0: # Lowest point of this band -> p_current_down - return unsafe_div(unsafe_div(p_o**2, p_o_up) * p_o, p_o_up) - if y == 0: # Highest point of this band -> p_current_up - p_o_up = unsafe_div(p_o_up * Aminus1, A) # now this is _actually_ p_o_down - return unsafe_div(p_o**2 / p_o_up * p_o, p_o_up) - - y0: uint256 = self._get_y0(x, y, p_o, p_o_up) - # ^ that call also checks that p_o != 0 - - # (f(y0) + x) / (g(y0) + y) - f: uint256 = unsafe_div(A * y0 * p_o, p_o_up) * p_o - g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o) - return (f + x * 10**18) / (g + y) - - -@external -@view -@nonreentrant('lock') -def get_p() -> uint256: - """ - @notice Get current AMM price in active_band - @return Current price at 1e18 base - """ - n: int256 = self.active_band - return self._get_p(n, self.bands_x[n], self.bands_y[n]) - - -@internal -@view -def _read_user_tick_numbers(user: address) -> int256[2]: - """ - @notice Unpacks and reads user tick numbers - @param user User address - @return Lowest and highest band the user deposited into - """ - ns: int256 = self.user_shares[user].ns - n2: int256 = unsafe_div(ns, 2**128) - n1: int256 = ns % 2**128 - if n1 >= 2**127: - n1 = unsafe_sub(n1, 2**128) - n2 = unsafe_add(n2, 1) - return [n1, n2] - - -@external -@view -@nonreentrant('lock') -def read_user_tick_numbers(user: address) -> int256[2]: - """ - @notice Unpacks and reads user tick numbers - @param user User address - @return Lowest and highest band the user deposited into - """ - return self._read_user_tick_numbers(user) - - -@internal -@view -def _read_user_ticks(user: address, ns: int256[2]) -> DynArray[uint256, MAX_TICKS_UINT]: - """ - @notice Unpacks and reads user ticks (shares) for all the ticks user deposited into - @param user User address - @param size Number of ticks the user deposited into - @return Array of shares the user has - """ - ticks: DynArray[uint256, MAX_TICKS_UINT] = [] - size: uint256 = convert(ns[1] - ns[0] + 1, uint256) - for i in range(MAX_TICKS / 2): - if len(ticks) == size: - break - tick: uint256 = self.user_shares[user].ticks[i] - ticks.append(tick & (2**128 - 1)) - if len(ticks) == size: - break - ticks.append(shift(tick, -128)) - return ticks - - -@external -@view -@nonreentrant('lock') -def can_skip_bands(n_end: int256) -> bool: - """ - @notice Check that we have no liquidity between active_band and `n_end` - """ - n: int256 = self.active_band - for i in range(MAX_SKIP_TICKS): - if n_end > n: - if self.bands_y[n] != 0: - return False - n = unsafe_add(n, 1) - else: - if self.bands_x[n] != 0: - return False - n = unsafe_sub(n, 1) - if n == n_end: # not including n_end - break - return True - # Actually skipping bands: - # * change self.active_band to the new n - # * change self.p_base_mul - # to do n2-n1 times (if n2 > n1): - # out.base_mul = unsafe_div(out.base_mul * Aminus1, A) - - -@external -@view -@nonreentrant('lock') -def active_band_with_skip() -> int256: - n0: int256 = self.active_band - n: int256 = n0 - min_band: int256 = self.min_band - for i in range(MAX_SKIP_TICKS): - if n < min_band: - n = n0 - MAX_SKIP_TICKS - break - if self.bands_x[n] != 0: - break - n -= 1 - return n - - -@external -@view -@nonreentrant('lock') -def has_liquidity(user: address) -> bool: - """ - @notice Check if `user` has any liquidity in the AMM - """ - return self.user_shares[user].ticks[0] != 0 - - -@internal -def save_user_shares(user: address, user_shares: DynArray[uint256, MAX_TICKS_UINT]): - ptr: uint256 = 0 - for j in range(MAX_TICKS_UINT / 2): - if ptr >= len(user_shares): - break - tick: uint256 = user_shares[ptr] - ptr = unsafe_add(ptr, 1) - if len(user_shares) != ptr: - tick = tick | shift(user_shares[ptr], 128) - ptr = unsafe_add(ptr, 1) - self.user_shares[user].ticks[j] = tick - - -@external -@nonreentrant('lock') -def deposit_range(user: address, amount: uint256, n1: int256, n2: int256): - """ - @notice Deposit for a user in a range of bands. Only admin contract (Controller) can do it - @param user User address - @param amount Amount of collateral to deposit - @param n1 Lower band in the deposit range - @param n2 Upper band in the deposit range - """ - assert msg.sender == self.admin - - user_shares: DynArray[uint256, MAX_TICKS_UINT] = [] - collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = [] - - n0: int256 = self.active_band - - # We assume that n1,n2 area already sorted (and they are in Controller) - assert n2 < 2**127 - assert n1 > -2**127 - - n_bands: uint256 = unsafe_add(convert(unsafe_sub(n2, n1), uint256), 1) - assert n_bands <= MAX_TICKS_UINT - - y_per_band: uint256 = unsafe_div(amount * COLLATERAL_PRECISION, n_bands) - assert y_per_band > 100, "Amount too low" - - assert self.user_shares[user].ticks[0] == 0 # dev: User must have no liquidity - self.user_shares[user].ns = unsafe_add(n1, unsafe_mul(n2, 2**128)) - - lm: LMGauge = self.liquidity_mining_callback - - # Autoskip bands if we can - for i in range(MAX_SKIP_TICKS + 1): - if n1 > n0: - if i != 0: - self.active_band = n0 - break - assert self.bands_x[n0] == 0 and i < MAX_SKIP_TICKS, "Deposit below current band" - n0 -= 1 - - for i in range(MAX_TICKS): - band: int256 = unsafe_add(n1, i) - if band > n2: - break - - assert self.bands_x[band] == 0, "Band not empty" - y: uint256 = y_per_band - if i == 0: - y = amount * COLLATERAL_PRECISION - y * unsafe_sub(n_bands, 1) - - total_y: uint256 = self.bands_y[band] - - # Total / user share - s: uint256 = self.total_shares[band] - ds: uint256 = unsafe_div((s + DEAD_SHARES) * y, total_y + 1) - assert ds > 0, "Amount too low" - user_shares.append(ds) - s += ds - assert s <= 2**128 - 1 - self.total_shares[band] = s - - total_y += y - self.bands_y[band] = total_y - - if lm.address != empty(address): - # If initial s == 0 - s becomes equal to y which is > 100 => nonzero - collateral_shares.append(unsafe_div(total_y * 10**18, s)) - - self.min_band = min(self.min_band, n1) - self.max_band = max(self.max_band, n2) - - self.save_user_shares(user, user_shares) - - log Deposit(user, amount, n1, n2) - - if lm.address != empty(address): - lm.callback_collateral_shares(n1, collateral_shares) - lm.callback_user_shares(user, n1, user_shares) - - -@external -@nonreentrant('lock') -def withdraw(user: address, frac: uint256) -> uint256[2]: - """ - @notice Withdraw liquidity for the user. Only admin contract can do it - @param user User who owns liquidity - @param frac Fraction to withdraw (1e18 being 100%) - @return Amount of [stablecoins, collateral] withdrawn - """ - assert msg.sender == self.admin - assert frac <= 10**18 - - lm: LMGauge = self.liquidity_mining_callback - - ns: int256[2] = self._read_user_tick_numbers(user) - n: int256 = ns[0] - user_shares: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns) - assert user_shares[0] > 0, "No deposits" - - total_x: uint256 = 0 - total_y: uint256 = 0 - min_band: int256 = self.min_band - old_min_band: int256 = min_band - old_max_band: int256 = self.max_band - max_band: int256 = n - 1 - - for i in range(MAX_TICKS): - x: uint256 = self.bands_x[n] - y: uint256 = self.bands_y[n] - ds: uint256 = unsafe_div(frac * user_shares[i], 10**18) - user_shares[i] = unsafe_sub(user_shares[i], ds) # Can ONLY zero out when frac == 10**18 - s: uint256 = self.total_shares[n] - new_shares: uint256 = s - ds - self.total_shares[n] = new_shares - s += DEAD_SHARES # after this s is guaranteed to be bigger than 0 - dx: uint256 = unsafe_div((x + 1) * ds, s) - dy: uint256 = unsafe_div((y + 1) * ds, s) - - x -= dx - y -= dy - - # If withdrawal is the last one - transfer dust to admin fees - if new_shares == 0: - if x > 0: - self.admin_fees_x += x - if y > 0: - self.admin_fees_y += unsafe_div(y, COLLATERAL_PRECISION) - x = 0 - y = 0 - - if n == min_band: - if x == 0: - if y == 0: - min_band += 1 - if x > 0 or y > 0: - max_band = n - self.bands_x[n] = x - self.bands_y[n] = y - total_x += dx - total_y += dy - - if n == ns[1]: - break - else: - n = unsafe_add(n, 1) - - # Empty the ticks - if frac == 10**18: - self.user_shares[user].ticks[0] = 0 - else: - self.save_user_shares(user, user_shares) - - if old_min_band != min_band: - self.min_band = min_band - if old_max_band <= ns[1]: - self.max_band = max_band - - total_x = unsafe_div(total_x, BORROWED_PRECISION) - total_y = unsafe_div(total_y, COLLATERAL_PRECISION) - log Withdraw(user, total_x, total_y) - - if lm.address != empty(address): - lm.callback_collateral_shares(0, []) # collateral/shares ratio is unchanged - lm.callback_user_shares(user, ns[0], user_shares) - - return [total_x, total_y] - - -@internal -@view -def calc_swap_out(pump: bool, in_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade: - """ - @notice Calculate the amount which can be obtained as a result of exchange. - If couldn't exchange all - will also update the amount which was actually used. - Also returns other parameters related to state after swap. - This function is core to the AMM functionality. - @param pump Indicates whether the trade buys or sells collateral - @param in_amount Amount of token going in - @param p_o Current oracle price and ratio (p_o, dynamic_fee) - @return Amounts spent and given out, initial and final bands of the AMM, new - amounts of coins in bands in the AMM, as well as admin fee charged, - all in one data structure - """ - # pump = True: borrowable (USD) in, collateral (ETH) out; going up - # pump = False: collateral (ETH) in, borrowable (USD) out; going down - min_band: int256 = self.min_band - max_band: int256 = self.max_band - out: DetailedTrade = empty(DetailedTrade) - out.n2 = self.active_band - p_o_up: uint256 = self._p_oracle_up(out.n2) - x: uint256 = self.bands_x[out.n2] - y: uint256 = self.bands_y[out.n2] - - in_amount_left: uint256 = in_amount - antifee: uint256 = unsafe_div( - (10**18)**2, - unsafe_sub(10**18, max(self.fee, p_o[1])) - ) - admin_fee: uint256 = self.admin_fee - j: uint256 = MAX_TICKS_UINT - - for i in range(MAX_TICKS + MAX_SKIP_TICKS): - y0: uint256 = 0 - f: uint256 = 0 - g: uint256 = 0 - Inv: uint256 = 0 - - if x > 0 or y > 0: - if j == MAX_TICKS_UINT: - out.n1 = out.n2 - j = 0 - y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o - f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18) - g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0]) - Inv = (f + x) * (g + y) - - if j != MAX_TICKS_UINT: - # Initialize - _tick: uint256 = y - if pump: - _tick = x - out.ticks_in.append(_tick) - - # Need this to break if price is too far - p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0]) - - if pump: - if y != 0: - if g != 0: - x_dest: uint256 = (unsafe_div(Inv, g) - f) - x - dx: uint256 = unsafe_div(x_dest * antifee, 10**18) - if dx >= in_amount_left: - # This is the last band - x_dest = unsafe_div(in_amount_left * 10**18, antifee) # LESS than in_amount_left - out.last_tick_j = min(Inv / (f + (x + x_dest)) - g + 1, y) # Should be always >= 0 - x_dest = unsafe_div(unsafe_sub(in_amount_left, x_dest) * admin_fee, 10**18) # abs admin fee now - x += in_amount_left # x is precise after this - # Round down the output - out.out_amount += y - out.last_tick_j - out.ticks_in[j] = x - x_dest - out.in_amount = in_amount - out.admin_fee = unsafe_add(out.admin_fee, x_dest) - break - - else: - # We go into the next band - dx = max(dx, 1) # Prevents from leaving dust in the band - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now - in_amount_left -= dx - out.ticks_in[j] = x + dx - x_dest - out.in_amount += dx - out.out_amount += y - out.admin_fee = unsafe_add(out.admin_fee, x_dest) - - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: - if out.n2 == max_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW): - # Don't allow to be away by more than ~50 ticks - break - out.n2 += 1 - p_o_up = unsafe_div(p_o_up * Aminus1, A) - x = 0 - y = self.bands_y[out.n2] - - else: # dump - if x != 0: - if f != 0: - y_dest: uint256 = (unsafe_div(Inv, f) - g) - y - dy: uint256 = unsafe_div(y_dest * antifee, 10**18) - if dy >= in_amount_left: - # This is the last band - y_dest = unsafe_div(in_amount_left * 10**18, antifee) - out.last_tick_j = min(Inv / (g + (y + y_dest)) - f + 1, x) - y_dest = unsafe_div(unsafe_sub(in_amount_left, y_dest) * admin_fee, 10**18) # abs admin fee now - y += in_amount_left - out.out_amount += x - out.last_tick_j - out.ticks_in[j] = y - y_dest - out.in_amount = in_amount - out.admin_fee = unsafe_add(out.admin_fee, y_dest) - break - - else: - # We go into the next band - dy = max(dy, 1) # Prevents from leaving dust in the band - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now - in_amount_left -= dy - out.ticks_in[j] = y + dy - y_dest - out.in_amount += dy - out.out_amount += x - out.admin_fee = unsafe_add(out.admin_fee, y_dest) - - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: - if out.n2 == min_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio > MAX_ORACLE_DN_POW: - # Don't allow to be away by more than ~50 ticks - break - out.n2 -= 1 - p_o_up = unsafe_div(p_o_up * A, Aminus1) - x = self.bands_x[out.n2] - y = 0 - - if j != MAX_TICKS_UINT: - j = unsafe_add(j, 1) - - # Round up what goes in and down what goes out - # ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION - out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision) - out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision) - - return out - - -@internal -@view -def _get_dxdy(i: uint256, j: uint256, amount: uint256, is_in: bool) -> DetailedTrade: - """ - @notice Method to use to calculate out amount and spent in amount - @param i Input coin index - @param j Output coin index - @param amount Amount of input or output coin to swap - @param is_in Whether IN our OUT amount is known - @return DetailedTrade with all swap results - """ - # i = 0: borrowable (USD) in, collateral (ETH) out; going up - # i = 1: collateral (ETH) in, borrowable (USD) out; going down - assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index" - out: DetailedTrade = empty(DetailedTrade) - if amount == 0: - return out - in_precision: uint256 = COLLATERAL_PRECISION - out_precision: uint256 = BORROWED_PRECISION - if i == 0: - in_precision = BORROWED_PRECISION - out_precision = COLLATERAL_PRECISION - p_o: uint256[2] = self._price_oracle_ro() - if is_in: - out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision) - else: - out = self.calc_swap_in(i == 0, amount * out_precision, p_o, in_precision, out_precision) - out.in_amount = unsafe_div(out.in_amount, in_precision) - out.out_amount = unsafe_div(out.out_amount, out_precision) - return out - - -@external -@view -@nonreentrant('lock') -def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: - """ - @notice Method to use to calculate out amount - @param i Input coin index - @param j Output coin index - @param in_amount Amount of input coin to swap - @return Amount of coin j to give out - """ - return self._get_dxdy(i, j, in_amount, True).out_amount - - -@external -@view -@nonreentrant('lock') -def get_dxdy(i: uint256, j: uint256, in_amount: uint256) -> (uint256, uint256): - """ - @notice Method to use to calculate out amount and spent in amount - @param i Input coin index - @param j Output coin index - @param in_amount Amount of input coin to swap - @return A tuple with in_amount used and out_amount returned - """ - out: DetailedTrade = self._get_dxdy(i, j, in_amount, True) - return (out.in_amount, out.out_amount) - - -@internal -def _exchange(i: uint256, j: uint256, amount: uint256, minmax_amount: uint256, _for: address, use_in_amount: bool) -> uint256[2]: - """ - @notice Exchanges two coins, callable by anyone - @param i Input coin index - @param j Output coin index - @param amount Amount of input/output coin to swap - @param minmax_amount Minimal/maximum amount to get as output/input - @param _for Address to send coins to - @param use_in_amount Whether input or output amount is specified - @return Amount of coins given in and out - """ - assert (i == 0 and j == 1) or (i == 1 and j == 0), "Wrong index" - p_o: uint256[2] = self._price_oracle_w() # Let's update the oracle even if we exchange 0 - if amount == 0: - return [0, 0] - - lm: LMGauge = self.liquidity_mining_callback - collateral_shares: DynArray[uint256, MAX_TICKS_UINT] = [] - - in_coin: ERC20 = BORROWED_TOKEN - out_coin: ERC20 = COLLATERAL_TOKEN - in_precision: uint256 = BORROWED_PRECISION - out_precision: uint256 = COLLATERAL_PRECISION - if i == 1: - in_precision = out_precision - in_coin = out_coin - out_precision = BORROWED_PRECISION - out_coin = BORROWED_TOKEN - - out: DetailedTrade = empty(DetailedTrade) - if use_in_amount: - out = self.calc_swap_out(i == 0, amount * in_precision, p_o, in_precision, out_precision) - else: - amount_to_swap: uint256 = max_value(uint256) - if amount < amount_to_swap: - amount_to_swap = amount * out_precision - out = self.calc_swap_in(i == 0, amount_to_swap, p_o, in_precision, out_precision) - in_amount_done: uint256 = unsafe_div(out.in_amount, in_precision) - out_amount_done: uint256 = unsafe_div(out.out_amount, out_precision) - if use_in_amount: - assert out_amount_done >= minmax_amount, "Slippage" - else: - assert in_amount_done <= minmax_amount and (out_amount_done == amount or amount == max_value(uint256)), "Slippage" - if out_amount_done == 0 or in_amount_done == 0: - return [0, 0] - - out.admin_fee = unsafe_div(out.admin_fee, in_precision) - if i == 0: - self.admin_fees_x += out.admin_fee - else: - self.admin_fees_y += out.admin_fee - - n: int256 = min(out.n1, out.n2) - n_start: int256 = n - n_diff: int256 = abs(unsafe_sub(out.n2, out.n1)) - - for k in range(MAX_TICKS): - x: uint256 = 0 - y: uint256 = 0 - if i == 0: - x = out.ticks_in[k] - if n == out.n2: - y = out.last_tick_j - else: - y = out.ticks_in[unsafe_sub(n_diff, k)] - if n == out.n2: - x = out.last_tick_j - self.bands_x[n] = x - self.bands_y[n] = y - if lm.address != empty(address): - s: uint256 = 0 - if y > 0: - s = unsafe_div(y * 10**18, self.total_shares[n]) - collateral_shares.append(s) - if k == n_diff: - break - n = unsafe_add(n, 1) - - self.active_band = out.n2 - - log TokenExchange(_for, i, in_amount_done, j, out_amount_done) - - if lm.address != empty(address): - lm.callback_collateral_shares(n_start, collateral_shares) - - assert in_coin.transferFrom(msg.sender, self, in_amount_done, default_return_value=True) - assert out_coin.transfer(_for, out_amount_done, default_return_value=True) - - return [in_amount_done, out_amount_done] - - -@internal -@view -def calc_swap_in(pump: bool, out_amount: uint256, p_o: uint256[2], in_precision: uint256, out_precision: uint256) -> DetailedTrade: - """ - @notice Calculate the input amount required to receive the desired output amount. - If couldn't exchange all - will also update the amount which was actually received. - Also returns other parameters related to state after swap. - @param pump Indicates whether the trade buys or sells collateral - @param out_amount Desired amount of token going out - @param p_o Current oracle price and antisandwich fee (p_o, dynamic_fee) - @return Amounts required and given out, initial and final bands of the AMM, new - amounts of coins in bands in the AMM, as well as admin fee charged, - all in one data structure - """ - # pump = True: borrowable (USD) in, collateral (ETH) out; going up - # pump = False: collateral (ETH) in, borrowable (USD) out; going down - min_band: int256 = self.min_band - max_band: int256 = self.max_band - out: DetailedTrade = empty(DetailedTrade) - out.n2 = self.active_band - p_o_up: uint256 = self._p_oracle_up(out.n2) - x: uint256 = self.bands_x[out.n2] - y: uint256 = self.bands_y[out.n2] - - out_amount_left: uint256 = out_amount - antifee: uint256 = unsafe_div( - (10**18)**2, - unsafe_sub(10**18, max(self.fee, p_o[1])) - ) - admin_fee: uint256 = self.admin_fee - j: uint256 = MAX_TICKS_UINT - - for i in range(MAX_TICKS + MAX_SKIP_TICKS): - y0: uint256 = 0 - f: uint256 = 0 - g: uint256 = 0 - Inv: uint256 = 0 - - if x > 0 or y > 0: - if j == MAX_TICKS_UINT: - out.n1 = out.n2 - j = 0 - y0 = self._get_y0(x, y, p_o[0], p_o_up) # <- also checks p_o - f = unsafe_div(A * y0 * p_o[0] / p_o_up * p_o[0], 10**18) - g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0]) - Inv = (f + x) * (g + y) - - if j != MAX_TICKS_UINT: - # Initialize - _tick: uint256 = y - if pump: - _tick = x - out.ticks_in.append(_tick) - - # Need this to break if price is too far - p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0]) - - if pump: - if y != 0: - if g != 0: - if y >= out_amount_left: - # This is the last band - out.last_tick_j = unsafe_sub(y, out_amount_left) - x_dest: uint256 = Inv / (g + out.last_tick_j) - f - x - dx: uint256 = unsafe_div(x_dest * antifee, 10**18) # MORE than x_dest - out.out_amount = out_amount # We successfully found liquidity for all the out_amount - out.in_amount += dx - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = x + dx - x_dest - out.admin_fee = unsafe_add(out.admin_fee, x_dest) - break - - else: - # We go into the next band - x_dest: uint256 = (unsafe_div(Inv, g) - f) - x - dx: uint256 = max(unsafe_div(x_dest * antifee, 10**18), 1) - out_amount_left -= y - out.in_amount += dx - out.out_amount += y - x_dest = unsafe_div(unsafe_sub(dx, x_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = x + dx - x_dest - out.admin_fee = unsafe_add(out.admin_fee, x_dest) - - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: - if out.n2 == max_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW): - # Don't allow to be away by more than ~50 ticks - break - out.n2 += 1 - p_o_up = unsafe_div(p_o_up * Aminus1, A) - x = 0 - y = self.bands_y[out.n2] - - else: # dump - if x != 0: - if f != 0: - if x >= out_amount_left: - # This is the last band - out.last_tick_j = unsafe_sub(x, out_amount_left) - y_dest: uint256 = Inv / (f + out.last_tick_j) - g - y - dy: uint256 = unsafe_div(y_dest * antifee, 10**18) # MORE than y_dest - out.out_amount = out_amount - out.in_amount += dy - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = y + dy - y_dest - out.admin_fee = unsafe_add(out.admin_fee, y_dest) - break - - else: - # We go into the next band - y_dest: uint256 = (unsafe_div(Inv, f) - g) - y - dy: uint256 = max(unsafe_div(y_dest * antifee, 10**18), 1) - out_amount_left -= x - out.in_amount += dy - out.out_amount += x - y_dest = unsafe_div(unsafe_sub(dy, y_dest) * admin_fee, 10**18) # abs admin fee now - out.ticks_in[j] = y + dy - y_dest - out.admin_fee = unsafe_add(out.admin_fee, y_dest) - - if i != MAX_TICKS + MAX_SKIP_TICKS - 1: - if out.n2 == min_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio > MAX_ORACLE_DN_POW: - # Don't allow to be away by more than ~50 ticks - break - out.n2 -= 1 - p_o_up = unsafe_div(p_o_up * A, Aminus1) - x = self.bands_x[out.n2] - y = 0 - - if j != MAX_TICKS_UINT: - j = unsafe_add(j, 1) - - # Round up what goes in and down what goes out - # ceil(in_amount_used/BORROWED_PRECISION) * BORROWED_PRECISION - out.in_amount = unsafe_mul(unsafe_div(unsafe_add(out.in_amount, unsafe_sub(in_precision, 1)), in_precision), in_precision) - out.out_amount = unsafe_mul(unsafe_div(out.out_amount, out_precision), out_precision) - - return out - - -@external -@view -@nonreentrant('lock') -def get_dx(i: uint256, j: uint256, out_amount: uint256) -> uint256: - """ - @notice Method to use to calculate in amount required to receive the desired out_amount - @param i Input coin index - @param j Output coin index - @param out_amount Desired amount of output coin to receive - @return Amount of coin i to spend - """ - # i = 0: borrowable (USD) in, collateral (ETH) out; going up - # i = 1: collateral (ETH) in, borrowable (USD) out; going down - trade: DetailedTrade = self._get_dxdy(i, j, out_amount, False) - assert trade.out_amount == out_amount - return trade.in_amount - - -@external -@view -@nonreentrant('lock') -def get_dydx(i: uint256, j: uint256, out_amount: uint256) -> (uint256, uint256): - """ - @notice Method to use to calculate in amount required and out amount received - @param i Input coin index - @param j Output coin index - @param out_amount Desired amount of output coin to receive - @return A tuple with out_amount received and in_amount returned - """ - # i = 0: borrowable (USD) in, collateral (ETH) out; going up - # i = 1: collateral (ETH) in, borrowable (USD) out; going down - out: DetailedTrade = self._get_dxdy(i, j, out_amount, False) - return (out.out_amount, out.in_amount) - - -@external -@nonreentrant('lock') -def exchange(i: uint256, j: uint256, in_amount: uint256, min_amount: uint256, _for: address = msg.sender) -> uint256[2]: - """ - @notice Exchanges two coins, callable by anyone - @param i Input coin index - @param j Output coin index - @param in_amount Amount of input coin to swap - @param min_amount Minimal amount to get as output - @param _for Address to send coins to - @return Amount of coins given in/out - """ - return self._exchange(i, j, in_amount, min_amount, _for, True) - - -@external -@nonreentrant('lock') -def exchange_dy(i: uint256, j: uint256, out_amount: uint256, max_amount: uint256, _for: address = msg.sender) -> uint256[2]: - """ - @notice Exchanges two coins, callable by anyone - @param i Input coin index - @param j Output coin index - @param out_amount Desired amount of output coin to receive - @param max_amount Maximum amount to spend (revert if more) - @param _for Address to send coins to - @return Amount of coins given in/out - """ - return self._exchange(i, j, out_amount, max_amount, _for, False) - - -@internal -@view -def get_xy_up(user: address, use_y: bool) -> uint256: - """ - @notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up, - or the amount of x (stablecoin) if we trade adiabatically down - @param user User the amount is calculated for - @param use_y Calculate amount of collateral if True and of stablecoin if False - @return Amount of coins - """ - ns: int256[2] = self._read_user_tick_numbers(user) - ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns) - if ticks[0] == 0: # Even dynamic array will have 0th element set here - return 0 - p_o: uint256 = self._price_oracle_ro()[0] - assert p_o != 0 - - n: int256 = ns[0] - 1 - n_active: int256 = self.active_band - p_o_down: uint256 = self._p_oracle_up(ns[0]) - XY: uint256 = 0 - - for i in range(MAX_TICKS): - n += 1 - if n > ns[1]: - break - x: uint256 = 0 - y: uint256 = 0 - if n >= n_active: - y = self.bands_y[n] - if n <= n_active: - x = self.bands_x[n] - # p_o_up: uint256 = self._p_oracle_up(n) - p_o_up: uint256 = p_o_down - # p_o_down = self._p_oracle_up(n + 1) - p_o_down = unsafe_div(p_o_down * Aminus1, A) - if x == 0: - if y == 0: - continue - - total_share: uint256 = self.total_shares[n] - user_share: uint256 = ticks[i] - if total_share == 0: - continue - if user_share == 0: - continue - total_share += DEAD_SHARES - # Also ideally we'd want to add +1 to all quantities when calculating with shares - # but we choose to save bytespace and slightly under-estimate the result of this call - # which is also more conservative - - # Also this will revert if p_o_down is 0, and p_o_down is 0 if p_o_up is 0 - p_current_mid: uint256 = unsafe_div(p_o**2 / p_o_down * p_o, p_o_up) - - # if p_o > p_o_up - we "trade" everything to y and then convert to the result - # if p_o < p_o_down - "trade" to x, then convert to result - # otherwise we are in-band, so we do the more complex logic to trade - # to p_o rather than to the edge of the band - # trade to the edge of the band == getting to the band edge while p_o=const - - # Cases when special conversion is not needed (to save on computations) - if x == 0 or y == 0: - if p_o > p_o_up: # p_o < p_current_down - # all to y at constant p_o, then to target currency adiabatically - y_equiv: uint256 = y - if y == 0: - y_equiv = x * 10**18 / p_current_mid - if use_y: - XY += unsafe_div(y_equiv * user_share, total_share) - else: - XY += unsafe_div(unsafe_div(y_equiv * p_o_up, SQRT_BAND_RATIO) * user_share, total_share) - continue - - elif p_o < p_o_down: # p_o > p_current_up - # all to x at constant p_o, then to target currency adiabatically - x_equiv: uint256 = x - if x == 0: - x_equiv = unsafe_div(y * p_current_mid, 10**18) - if use_y: - XY += unsafe_div(unsafe_div(x_equiv * SQRT_BAND_RATIO, p_o_up) * user_share, total_share) - else: - XY += unsafe_div(x_equiv * user_share, total_share) - continue - - # If we are here - we need to "trade" to somewhere mid-band - # So we need more heavy math - - y0: uint256 = self._get_y0(x, y, p_o, p_o_up) - f: uint256 = unsafe_div(unsafe_div(A * y0 * p_o, p_o_up) * p_o, 10**18) - g: uint256 = unsafe_div(Aminus1 * y0 * p_o_up, p_o) - # (f + x)(g + y) = const = p_top * A**2 * y0**2 = I - Inv: uint256 = (f + x) * (g + y) - # p = (f + x) / (g + y) => p * (g + y)**2 = I or (f + x)**2 / p = I - - # First, "trade" in this band to p_oracle - x_o: uint256 = 0 - y_o: uint256 = 0 - - if p_o > p_o_up: # p_o < p_current_down, all to y - # x_o = 0 - y_o = unsafe_sub(max(Inv / f, g), g) - if use_y: - XY += unsafe_div(y_o * user_share, total_share) - else: - XY += unsafe_div(unsafe_div(y_o * p_o_up, SQRT_BAND_RATIO) * user_share, total_share) - - elif p_o < p_o_down: # p_o > p_current_up, all to x - # y_o = 0 - x_o = unsafe_sub(max(Inv / g, f), f) - if use_y: - XY += unsafe_div(unsafe_div(x_o * SQRT_BAND_RATIO, p_o_up) * user_share, total_share) - else: - XY += unsafe_div(x_o * user_share, total_share) - - else: - # Equivalent from Chainsecurity (which also has less numerical errors): - y_o = unsafe_div(A * y0 * unsafe_sub(p_o, p_o_down), p_o) - # x_o = unsafe_div(A * y0 * p_o, p_o_up) * unsafe_sub(p_o_up, p_o) - # Old math - # y_o = unsafe_sub(max(self.sqrt_int(unsafe_div(Inv * 10**18, p_o)), g), g) - x_o = unsafe_sub(max(Inv / (g + y_o), f), f) - - # Now adiabatic conversion from definitely in-band - if use_y: - XY += unsafe_div((y_o + x_o * 10**18 / self.sqrt_int(p_o_up * p_o)) * user_share, total_share) - - else: - XY += unsafe_div((x_o + unsafe_div(y_o * self.sqrt_int(p_o_down * p_o), 10**18)) * user_share, total_share) - - if use_y: - return unsafe_div(XY, COLLATERAL_PRECISION) - else: - return unsafe_div(XY, BORROWED_PRECISION) - - -@external -@view -@nonreentrant('lock') -def get_y_up(user: address) -> uint256: - """ - @notice Measure the amount of y (collateral) in the band n if we adiabatically trade near p_oracle on the way up - @param user User the amount is calculated for - @return Amount of coins - """ - return self.get_xy_up(user, True) - - -@external -@view -@nonreentrant('lock') -def get_x_down(user: address) -> uint256: - """ - @notice Measure the amount of x (stablecoin) if we trade adiabatically down - @param user User the amount is calculated for - @return Amount of coins - """ - return self.get_xy_up(user, False) - -@internal -@view -def _get_xy(user: address, is_sum: bool) -> DynArray[uint256, MAX_TICKS_UINT][2]: - """ - @notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns - @param user User address - @param is_sum Return sum or amounts by bands - @return Amounts of (stablecoin, collateral) in a tuple - """ - xs: DynArray[uint256, MAX_TICKS_UINT] = [] - ys: DynArray[uint256, MAX_TICKS_UINT] = [] - if is_sum: - xs.append(0) - ys.append(0) - ns: int256[2] = self._read_user_tick_numbers(user) - ticks: DynArray[uint256, MAX_TICKS_UINT] = self._read_user_ticks(user, ns) - if ticks[0] != 0: - for i in range(MAX_TICKS): - total_shares: uint256 = self.total_shares[ns[0]] + DEAD_SHARES - ds: uint256 = ticks[i] - dx: uint256 = unsafe_div((self.bands_x[ns[0]] + 1) * ds, total_shares) - dy: uint256 = unsafe_div((self.bands_y[ns[0]] + 1) * ds, total_shares) - if is_sum: - xs[0] += dx - ys[0] += dy - else: - xs.append(unsafe_div(dx, BORROWED_PRECISION)) - ys.append(unsafe_div(dy, COLLATERAL_PRECISION)) - if ns[0] == ns[1]: - break - ns[0] = unsafe_add(ns[0], 1) - - if is_sum: - xs[0] = unsafe_div(xs[0], BORROWED_PRECISION) - ys[0] = unsafe_div(ys[0], COLLATERAL_PRECISION) - - return [xs, ys] - -@external -@view -@nonreentrant('lock') -def get_sum_xy(user: address) -> uint256[2]: - """ - @notice A low-gas function to measure amounts of stablecoins and collateral which user currently owns - @param user User address - @return Amounts of (stablecoin, collateral) in a tuple - """ - xy: DynArray[uint256, MAX_TICKS_UINT][2] = self._get_xy(user, True) - return [xy[0][0], xy[1][0]] - -@external -@view -@nonreentrant('lock') -def get_xy(user: address) -> DynArray[uint256, MAX_TICKS_UINT][2]: - """ - @notice A low-gas function to measure amounts of stablecoins and collateral by bands which user currently owns - @param user User address - @return Amounts of (stablecoin, collateral) by bands in a tuple - """ - return self._get_xy(user, False) - - -@external -@view -@nonreentrant('lock') -def get_amount_for_price(p: uint256) -> (uint256, bool): - """ - @notice Amount necessary to be exchanged to have the AMM at the final price `p` - @return (amount, is_pump) - """ - min_band: int256 = self.min_band - max_band: int256 = self.max_band - n: int256 = self.active_band - p_o: uint256[2] = self._price_oracle_ro() - p_o_up: uint256 = self._p_oracle_up(n) - p_down: uint256 = unsafe_div(unsafe_div(p_o[0]**2, p_o_up) * p_o[0], p_o_up) # p_current_down - p_up: uint256 = unsafe_div(p_down * A2, Aminus12) # p_crurrent_up - amount: uint256 = 0 - y0: uint256 = 0 - f: uint256 = 0 - g: uint256 = 0 - Inv: uint256 = 0 - j: uint256 = MAX_TICKS_UINT - pump: bool = True - - for i in range(MAX_TICKS + MAX_SKIP_TICKS): - assert p_o_up > 0 - x: uint256 = self.bands_x[n] - y: uint256 = self.bands_y[n] - if i == 0: - if p < self._get_p(n, x, y): - pump = False - not_empty: bool = x > 0 or y > 0 - if not_empty: - y0 = self._get_y0(x, y, p_o[0], p_o_up) - f = unsafe_div(unsafe_div(A * y0 * p_o[0], p_o_up) * p_o[0], 10**18) - g = unsafe_div(Aminus1 * y0 * p_o_up, p_o[0]) - Inv = (f + x) * (g + y) - if j == MAX_TICKS_UINT: - j = 0 - - if p <= p_up: - if p >= p_down: - if not_empty: - ynew: uint256 = unsafe_sub(max(self.sqrt_int(Inv * 10**18 / p), g), g) - xnew: uint256 = unsafe_sub(max(Inv / (g + ynew), f), f) - if pump: - amount += unsafe_sub(max(xnew, x), x) - else: - amount += unsafe_sub(max(ynew, y), y) - break - - # Need this to break if price is too far - p_ratio: uint256 = unsafe_div(p_o_up * 10**18, p_o[0]) - - if pump: - if not_empty: - amount += (Inv / g - f) - x - if n == max_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio < unsafe_div(10**36, MAX_ORACLE_DN_POW): - # Don't allow to be away by more than ~50 ticks - break - n += 1 - p_down = p_up - p_up = unsafe_div(p_up * A2, Aminus12) - p_o_up = unsafe_div(p_o_up * Aminus1, A) - - else: - if not_empty: - amount += (Inv / f - g) - y - if n == min_band: - break - if j == MAX_TICKS_UINT - 1: - break - if p_ratio > MAX_ORACLE_DN_POW: - # Don't allow to be away by more than ~50 ticks - break - n -= 1 - p_up = p_down - p_down = unsafe_div(p_down * Aminus12, A2) - p_o_up = unsafe_div(p_o_up * A, Aminus1) - - if j != MAX_TICKS_UINT: - j = unsafe_add(j, 1) - - amount = amount * 10**18 / unsafe_sub(10**18, max(self.fee, p_o[1])) - if amount == 0: - return 0, pump - - # Precision and round up - if pump: - amount = unsafe_add(unsafe_div(unsafe_sub(amount, 1), BORROWED_PRECISION), 1) - else: - amount = unsafe_add(unsafe_div(unsafe_sub(amount, 1), COLLATERAL_PRECISION), 1) - - return amount, pump - - -@external -@nonreentrant('lock') -def set_rate(rate: uint256) -> uint256: - """ - @notice Set interest rate. That affects the dependence of AMM base price over time - @param rate New rate in units of int(fraction * 1e18) per second - @return rate_mul multiplier (e.g. 1.0 + integral(rate, dt)) - """ - assert msg.sender == self.admin - rate_mul: uint256 = self._rate_mul() - self.rate_mul = rate_mul - self.rate_time = block.timestamp - self.rate = rate - log SetRate(rate, rate_mul, block.timestamp) - return rate_mul - - -@external -@nonreentrant('lock') -def set_fee(fee: uint256): - """ - @notice Set AMM fee - @param fee Fee where 1e18 == 100% - """ - assert msg.sender == self.admin - self.fee = fee - log SetFee(fee) - - -@external -@nonreentrant('lock') -def set_admin_fee(fee: uint256): - """ - @notice Set admin fee - fraction of the AMM fee to go to admin - @param fee Admin fee where 1e18 == 100% - """ - assert msg.sender == self.admin - self.admin_fee = fee - log SetAdminFee(fee) - - -@external -@nonreentrant('lock') -def reset_admin_fees(): - """ - @notice Zero out AMM fees collected - """ - assert msg.sender == self.admin - self.admin_fees_x = 0 - self.admin_fees_y = 0 - - -# nonreentrant decorator is in Controller which is admin -@external -def set_callback(liquidity_mining_callback: LMGauge): - """ - @notice Set a gauge address with callbacks for liquidity mining for collateral - @param liquidity_mining_callback Gauge address - """ - assert msg.sender == self.admin - self.liquidity_mining_callback = liquidity_mining_callback diff --git a/contracts/testing/OptimizeMath.vy b/contracts/testing/OptimizeMath.vy deleted file mode 100644 index f71bd442..00000000 --- a/contracts/testing/OptimizeMath.vy +++ /dev/null @@ -1,338 +0,0 @@ -# @version 0.3.10 - -EXP_PRECISION: constant(uint256) = 10**10 - - -@external -@view -def original_log2(_x: uint256) -> uint256: - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - res: uint256 = 0 - x: uint256 = _x - for i in range(8): - t: uint256 = 2**(7 - i) - p: uint256 = 2**t - if x >= p * 10**18: - x /= p - res += t * 10**18 - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res += d - x /= 2 - x = x * x / 10**18 - d /= 2 - return res - - -@external -@view -def optimized_log2(_x: uint256) -> int256: - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - inverse: bool = _x < 10**18 - res: uint256 = 0 - x: uint256 = _x - if inverse: - x = 10**36 / x - t: uint256 = 2**7 - for i in range(8): - p: uint256 = pow_mod256(2, t) - if x >= unsafe_mul(p, 10**18): - x = unsafe_div(x, p) - res = unsafe_add(unsafe_mul(t, 10**18), res) - t = unsafe_div(t, 2) - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res = unsafe_add(res, d) - x = unsafe_div(x, 2) - x = unsafe_div(unsafe_mul(x, x), 10**18) - d = unsafe_div(d, 2) - if inverse: - return -convert(res, int256) - else: - return convert(res, int256) - - -@external -@view -def original_sqrt(x: uint256) -> uint256: - """ - Originating from: https://github.com/vyperlang/vyper/issues/1266 - """ - - if x == 0: - return 0 - - z: uint256 = (x + 10**18) / 2 - y: uint256 = x - - for i in range(256): - if z == y: - return y - y = z - z = (x * 10**18 / z + z) / 2 - - raise "Did not converge" - - -@external -@view -def optimized_sqrt(x: uint256) -> uint256: - """ - Originating from: https://github.com/vyperlang/vyper/issues/1266 - """ - assert x < max_value(uint256) / 10**18 + 1 - if x == 0: - return 0 - - z: uint256 = unsafe_div(unsafe_add(x, 10**18), 2) - y: uint256 = x - - for i in range(256): - if z == y: - return y - y = z - z = unsafe_div(unsafe_add(unsafe_div(unsafe_mul(x, 10**18), z), z), 2) - - raise "Did not converge" - - -@external -@view -def optimized_sqrt_initial(x: uint256, y0: uint256) -> uint256: - """ - Originating from: https://github.com/vyperlang/vyper/issues/1266 - """ - if x == 0: - return 0 - _x: uint256 = x * 10**18 - - z: uint256 = y0 - y: uint256 = 0 - if z == 0: - z = unsafe_div(unsafe_add(x, 10**18), 2) - - for i in range(256): - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - if z == y: - return y - y = z - - raise "Did not converge" - -@external -@view -def optimized_sqrt_solmate(x: uint256) -> uint256: - # https://github.com/transmissions11/solmate/blob/v7/src/utils/FixedPointMathLib.sol#L288 - _x: uint256 = x * 10**18 - y: uint256 = _x - z: uint256 = 181 - if y >= 2**(128 + 8): - y = unsafe_div(y, 2**128) - z = unsafe_mul(z, 2**64) - if y >= 2**(64 + 8): - y = unsafe_div(y, 2**64) - z = unsafe_mul(z, 2**32) - if y >= 2**(32 + 8): - y = unsafe_div(y, 2**32) - z = unsafe_mul(z, 2**16) - if y >= 2**(16 + 8): - y = unsafe_div(y, 2**16) - z = unsafe_mul(z, 2**8) - - z = unsafe_div(unsafe_mul(z, unsafe_add(y, 65536)), 2**18) - - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - z = unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - return unsafe_div(unsafe_add(unsafe_div(_x, z), z), 2) - - -@external -@view -def halfpow(power: uint256) -> uint256: - """ - 1e18 * 0.5 ** (power/1e18) - - Inspired by: https://github.com/balancer-labs/balancer-core/blob/master/contracts/BNum.sol#L128 - - Better result can by achived with: - https://github.com/transmissions11/solmate/blob/v7/src/utils/FixedPointMathLib.sol#L34 - """ - intpow: uint256 = unsafe_div(power, 10**18) - if intpow > 59: - return 0 - otherpow: uint256 = unsafe_sub(power, unsafe_mul(intpow, 10**18)) # < 10**18 - result: uint256 = unsafe_div(10**18, pow_mod256(2, intpow)) - if otherpow == 0: - return result - - term: uint256 = 10**18 - S: uint256 = 10**18 - c: uint256 = otherpow - - for i in range(1, 256): - K: uint256 = unsafe_mul(i, 10**18) # <= 255 * 10**18; >= 10**18 - term = unsafe_div(unsafe_mul(term, unsafe_div(c, 2)), K) - S = unsafe_sub(S, term) - if term < EXP_PRECISION: - return unsafe_div(unsafe_mul(result, S), 10**18) - c = unsafe_sub(K, otherpow) - - raise "Did not converge" - - -@external -@view -def optimized_exp(power: int256) -> uint256: - if power <= -41446531673892821376: - return 0 - - if power >= 135305999368893231589: - raise "exp overflow" - - x: int256 = unsafe_div(unsafe_mul(power, 2**96), 10**18) - - k: int256 = unsafe_div( - unsafe_add( - unsafe_div(unsafe_mul(x, 2**96), 54916777467707473351141471128), - 2**95), - 2**96) - x = unsafe_sub(x, unsafe_mul(k, 54916777467707473351141471128)) - - y: int256 = unsafe_add(x, 1346386616545796478920950773328) - y = unsafe_add(unsafe_div(unsafe_mul(y, x), 2**96), 57155421227552351082224309758442) - p: int256 = unsafe_sub(unsafe_add(y, x), 94201549194550492254356042504812) - p = unsafe_add(unsafe_div(unsafe_mul(p, y), 2**96), 28719021644029726153956944680412240) - p = unsafe_add(unsafe_mul(p, x), (4385272521454847904659076985693276 * 2**96)) - - q: int256 = x - 2855989394907223263936484059900 - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 50020603652535783019961831881945) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 533845033583426703283633433725380) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 3604857256930695427073651918091429) - q = unsafe_sub(unsafe_div(unsafe_mul(q, x), 2**96), 14423608567350463180887372962807573) - q = unsafe_add(unsafe_div(unsafe_mul(q, x), 2**96), 26449188498355588339934803723976023) - - return shift( - unsafe_mul(convert(unsafe_div(p, q), uint256), 3822833074963236453042738258902158003155416615667), - unsafe_sub(k, 195)) - - -@internal -@pure -def _log_2(x: uint256) -> uint256: - """ - @dev An `internal` helper function that returns the log in base 2 - of `x`, following the selected rounding direction. - @notice Note that it returns 0 if given 0. The implementation is - inspired by OpenZeppelin's implementation here: - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol. - This code is taken from snekmate. - @param x The 32-byte variable. - @return uint256 The 32-byte calculation result. - """ - value: uint256 = x - result: uint256 = empty(uint256) - - # The following lines cannot overflow because we have the well-known - # decay behaviour of `log_2(max_value(uint256)) < max_value(uint256)`. - if (x >> 128 != empty(uint256)): - value = x >> 128 - result = 128 - if (value >> 64 != empty(uint256)): - value = value >> 64 - result = unsafe_add(result, 64) - if (value >> 32 != empty(uint256)): - value = value >> 32 - result = unsafe_add(result, 32) - if (value >> 16 != empty(uint256)): - value = value >> 16 - result = unsafe_add(result, 16) - if (value >> 8 != empty(uint256)): - value = value >> 8 - result = unsafe_add(result, 8) - if (value >> 4 != empty(uint256)): - value = value >> 4 - result = unsafe_add(result, 4) - if (value >> 2 != empty(uint256)): - value = value >> 2 - result = unsafe_add(result, 2) - if (value >> 1 != empty(uint256)): - result = unsafe_add(result, 1) - - return result - - -@external -@pure -def wad_ln(x: uint256) -> int256: - """ - @dev Calculates the natural logarithm of a signed integer with a - precision of 1e18. - @notice Note that it returns 0 if given 0. Furthermore, this function - consumes about 1,400 to 1,650 gas units depending on the value - of `x`. The implementation is inspired by Remco Bloemen's - implementation under the MIT license here: - https://xn--2-umb.com/22/exp-ln. - This code is taken from snekmate. - @param x The 32-byte variable. - @return int256 The 32-byte calculation result. - """ - value: int256 = convert(x, int256) - - assert x > 0 - - # We want to convert `x` from "10 ** 18" fixed point to "2 ** 96" - # fixed point. We do this by multiplying by "2 ** 96 / 10 ** 18". - # But since "ln(x * C) = ln(x) + ln(C)" holds, we can just do nothing - # here and add "ln(2 ** 96 / 10 ** 18)" at the end. - - # Reduce the range of `x` to "(1, 2) * 2 ** 96". - # Also remember that "ln(2 ** k * x) = k * ln(2) + ln(x)" holds. - k: int256 = unsafe_sub(convert(self._log_2(x), int256), 96) - # Note that to circumvent Vyper's safecast feature for the potentially - # negative expression `value <<= uint256(159 - k)`, we first convert the - # expression `value <<= uint256(159 - k)` to `bytes32` and subsequently - # to `uint256`. Remember that the EVM default behaviour is to use two's - # complement representation to handle signed integers. - value = convert(convert(convert(value << convert(unsafe_sub(159, k), uint256), bytes32), uint256) >> 159, int256) - - # Evaluate using a "(8, 8)"-term rational approximation. Since `p` is monic, - # we will multiply by a scaling factor later. - p: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 3_273_285_459_638_523_848_632_254_066_296), value) >> 96, 24_828_157_081_833_163_892_658_089_445_524) - p = unsafe_add(unsafe_mul(p, value) >> 96, 43_456_485_725_739_037_958_740_375_743_393) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 11_111_509_109_440_967_052_023_855_526_967) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 45_023_709_667_254_063_763_336_534_515_857) - p = unsafe_sub(unsafe_mul(p, value) >> 96, 14_706_773_417_378_608_786_704_636_184_526) - p = unsafe_sub(unsafe_mul(p, value), 795_164_235_651_350_426_258_249_787_498 << 96) - - # We leave `p` in the "2 ** 192" base so that we do not have to scale it up - # again for the division. Note that `q` is monic by convention. - q: int256 = unsafe_add(unsafe_mul(unsafe_add(value, 5_573_035_233_440_673_466_300_451_813_936), value) >> 96, 71_694_874_799_317_883_764_090_561_454_958) - q = unsafe_add(unsafe_mul(q, value) >> 96, 283_447_036_172_924_575_727_196_451_306_956) - q = unsafe_add(unsafe_mul(q, value) >> 96, 401_686_690_394_027_663_651_624_208_769_553) - q = unsafe_add(unsafe_mul(q, value) >> 96, 204_048_457_590_392_012_362_485_061_816_622) - q = unsafe_add(unsafe_mul(q, value) >> 96, 31_853_899_698_501_571_402_653_359_427_138) - q = unsafe_add(unsafe_mul(q, value) >> 96, 909_429_971_244_387_300_277_376_558_375) - - # It is known that the polynomial `q` has no zeros in the domain. - # No scaling is required, as `p` is already "2 ** 96" too large. Also, - # `r` is in the range "(0, 0.125) * 2 ** 96" after the division. - r: int256 = unsafe_div(p, q) - - # To finalise the calculation, we have to proceed with the following steps: - # - multiply by the scaling factor "s = 5.549...", - # - add "ln(2 ** 96 / 10 ** 18)", - # - add "k * ln(2)", and - # - multiply by "10 ** 18 / 2 ** 96 = 5 ** 18 >> 78". - # In order to perform the most gas-efficient calculation, we carry out all - # these steps in one expression. - return unsafe_add(unsafe_add(unsafe_mul(r, 1_677_202_110_996_718_588_342_820_967_067_443_963_516_166),\ - unsafe_mul(k, 16_597_577_552_685_614_221_487_285_958_193_947_469_193_820_559_219_878_177_908_093_499_208_371)),\ - 600_920_179_829_731_861_736_702_779_321_621_459_595_472_258_049_074_101_567_377_883_020_018_308) >> 174 diff --git a/contracts/testing/SwapFactory.vy b/contracts/testing/SwapFactory.vy index 80098268..786e31b7 100644 --- a/contracts/testing/SwapFactory.vy +++ b/contracts/testing/SwapFactory.vy @@ -1,4 +1,6 @@ -# @version 0.3.10 +# pragma version 0.4.3 + +MAX_COINS: constant(uint256) = 4 interface Swap: def initialize( @@ -12,10 +14,18 @@ interface Swap: def factory() -> address: view interface FactoryNG: - def deploy_plain_pool(_name: String[32], _symbol: String[10], _coins: DynArray[address, MAX_COINS], _A: uint256, - _fee: uint256, _offpeg_fee_multiplier: uint256, _ma_exp_time: uint256, - _implementation_idx: uint256, _asset_types: DynArray[uint8, MAX_COINS], - _method_ids: DynArray[bytes4, MAX_COINS], _oracles: DynArray[address, MAX_COINS], + def deploy_plain_pool( + _name: String[32], + _symbol: String[10], + _coins: DynArray[address, MAX_COINS], + _A: uint256, + _fee: uint256, + _offpeg_fee_multiplier: uint256, + _ma_exp_time: uint256, + _implementation_idx: uint256, + _asset_types: DynArray[uint8, MAX_COINS], + _method_ids: DynArray[bytes4, MAX_COINS], + _oracles: DynArray[address, MAX_COINS], ) -> address: nonpayable interface ERC20: @@ -25,7 +35,6 @@ interface ERC20: def approve(_spender: address, _amount: uint256): nonpayable -MAX_COINS: constant(uint256) = 4 IMPL: immutable(address) n: public(uint256) pools: public(HashMap[uint256, address]) @@ -34,7 +43,7 @@ admin: public(address) factory_ng: FactoryNG rate_oracle: address -@external +@deploy def __init__(impl: address): IMPL = impl self.admin = msg.sender @@ -43,12 +52,14 @@ def __init__(impl: address): @external def deploy(coin_a: ERC20, coin_b: ERC20) -> address: pool: Swap = Swap(create_minimal_proxy_to(IMPL)) - pool.initialize( - 'TestName', 'TST', + extcall pool.initialize( + "TestName", + "TST", [coin_a.address, coin_b.address, empty(address), empty(address)], - [10**(18-coin_a.decimals()) * 10**18, 10**(18-coin_b.decimals()) * 10**18, 0, 0], + [10**(18 - staticcall coin_a.decimals()) * 10**18, 10**(18 - staticcall coin_b.decimals()) * 10**18, 0, 0], 100, - 0) + 0, + ) self.pools[self.n] = pool.address self.n += 1 return pool.address @@ -57,10 +68,16 @@ def deploy(coin_a: ERC20, coin_b: ERC20) -> address: @external def deploy_ng(coin_a: ERC20, coin_b: ERC20) -> address: assert self.factory_ng.address != empty(address), "Factory not set" - pool: address = self.factory_ng.deploy_plain_pool( - 'TestName-ng', 'TST-ng', + pool: address = extcall self.factory_ng.deploy_plain_pool( + "TestName-ng", + "TST-ng", [coin_a.address, coin_b.address], - 100, 0, 10000000000, 866, 0, [1, 1], + 100, + 0, + 10000000000, + 866, + 0, + [1, 1], [method_id("get00()", output_type=bytes4), method_id("get11()", output_type=bytes4)], [self.rate_oracle, self.rate_oracle], ) diff --git a/contracts/testing/TricryptoMock.vy b/contracts/testing/TricryptoMock.vy index d29b23f1..946e3221 100644 --- a/contracts/testing/TricryptoMock.vy +++ b/contracts/testing/TricryptoMock.vy @@ -1,9 +1,9 @@ -# @version 0.3.10 +# pragma version 0.4.3 coins: public(address[3]) price_oracle: public(uint256[3]) -@external +@deploy def __init__(coins: address[3]): self.coins = coins diff --git a/contracts/testing/VEDelegationMock.vy b/contracts/testing/VEDelegationMock.vy deleted file mode 100644 index 76b2f3a1..00000000 --- a/contracts/testing/VEDelegationMock.vy +++ /dev/null @@ -1,18 +0,0 @@ -# @version 0.3.10 -""" -@title veBoost Mock -""" -from vyper.interfaces import ERC20 - -VOTING_ESCROW: immutable(address) - - -@external -def __init__(_voting_escrow: address): - VOTING_ESCROW = _voting_escrow - - -@view -@external -def adjusted_balance_of(_user: address) -> uint256: - return ERC20(VOTING_ESCROW).balanceOf(_user) diff --git a/contracts/testing/WETH.vy b/contracts/testing/WETH.vy index 0ce0ffa2..797695a8 100644 --- a/contracts/testing/WETH.vy +++ b/contracts/testing/WETH.vy @@ -1,4 +1,4 @@ -# @version ^0.3.9 +# pragma version 0.4.3 """ @notice Mock ERC20 for testing """ @@ -21,8 +21,7 @@ allowances: HashMap[address, HashMap[address, uint256]] total_supply: uint256 -@payable -@external +@deploy def __init__(): self.name = "Wrapped Ether" self.symbol = "WETH" @@ -45,7 +44,7 @@ def allowance(_owner : address, _spender : address) -> uint256: def transfer(_to : address, _value : uint256) -> bool: self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value - log Transfer(msg.sender, _to, _value) + log Transfer(_from=msg.sender, _to=_to, _value=_value) return True @@ -54,14 +53,14 @@ def transferFrom(_from : address, _to : address, _value : uint256) -> bool: self.balanceOf[_from] -= _value self.balanceOf[_to] += _value self.allowances[_from][msg.sender] -= _value - log Transfer(_from, _to, _value) + log Transfer(_from=_from, _to=_to, _value=_value) return True @external def approve(_spender : address, _value : uint256) -> bool: self.allowances[msg.sender][_spender] = _value - log Approval(msg.sender, _spender, _value) + log Approval(_owner=msg.sender, _spender=_spender, _value=_value) return True @@ -69,7 +68,7 @@ def approve(_spender : address, _value : uint256) -> bool: def _mint_for_testing(_target: address, _value: uint256) -> bool: self.total_supply += _value self.balanceOf[_target] += _value - log Transfer(empty(address), _target, _value) + log Transfer(_from=empty(address), _to=_target, _value=_value) return True diff --git a/contracts/testing/zaps/PartialRepayZapTester.vy b/contracts/testing/zaps/PartialRepayZapTester.vy new file mode 100644 index 00000000..ed4586f0 --- /dev/null +++ b/contracts/testing/zaps/PartialRepayZapTester.vy @@ -0,0 +1,10 @@ +# pragma version 0.4.3 + +from curve_std.interfaces import IERC20 +from curve_std import token as tkn + + +@external +def callback_liquidate_partial(calldata: Bytes[4 * 10**4 - 32 * 6 - 16]): + borrowed: IERC20 = IERC20(convert(slice(calldata, 0, 20), address)) + tkn.max_approve(borrowed, msg.sender) diff --git a/contracts/zaps/CreateFromPool.vy b/contracts/zaps/CreateFromPool.vy new file mode 100644 index 00000000..4f547f32 --- /dev/null +++ b/contracts/zaps/CreateFromPool.vy @@ -0,0 +1,95 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on +""" +@title Create From Pool Factory Helper +@notice Disposable contract to create lending markets using existing Curve pools as price oracles. + Needs to be redeployed to change pool_price_oracle_blueprint. +@author Curve.fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@custom:security security@curve.fi +""" +from curve_std.interfaces import IERC20 +from contracts.interfaces import ILendingFactory +from contracts.interfaces import IMonetaryPolicy +from contracts.interfaces import IPriceOracle + +FACTORY: immutable(ILendingFactory) +POOL_PRICE_ORACLE_BLUEPRINT: public(immutable(address)) + + +@deploy +def __init__(_factory: ILendingFactory, _pool_price_oracle_blueprint: address): + FACTORY = _factory + POOL_PRICE_ORACLE_BLUEPRINT = _pool_price_oracle_blueprint + + +@external +def create_from_pool( + _borrowed_token: IERC20, + _collateral_token: IERC20, + _A: uint256, + _fee: uint256, + _loan_discount: uint256, + _liquidation_discount: uint256, + _monetary_policy: IMonetaryPolicy, + _pool: address, + _name: String[64], + _supply_limit: uint256 = max_value(uint256), +) -> address[3]: + """ + @notice Creation of the vault using existing oraclized Curve pool as a price oracle + @param _borrowed_token Token which is being borrowed + @param _collateral_token Token used for collateral + @param _A Amplification coefficient: band size is ~1//A + @param _fee Fee for swaps in AMM (for ETH markets found to be 0.6%) + @param _loan_discount Maximum discount. LTV = sqrt(((A - 1) // A) ** 4) - loan_discount + @param _liquidation_discount Liquidation discount. LT = sqrt(((A - 1) // A) ** 4) - liquidation_discount + @param _monetary_policy Monetary policy contract for the market + @param _pool Curve tricrypto-ng, twocrypto-ng or stableswap-ng pool which has non-manipulatable price_oracle(). + Must contain both collateral_token and borrowed_token. + @param _name Human-readable market name + @param _supply_limit Supply cap + """ + # Find coins in the pool + borrowed_ix: uint256 = 100 + collateral_ix: uint256 = 100 + # TODO duplicated code + N: uint256 = 0 + for i: uint256 in range(10): + success: bool = False + res: Bytes[32] = empty(Bytes[32]) + success, res = raw_call( + _pool, + abi_encode(i, method_id=method_id("coins(uint256)")), + max_outsize=32, + is_static_call=True, + revert_on_failure=False, + ) + coin: IERC20 = IERC20(convert(res, address)) + if not success or coin == empty(IERC20): + break + N += 1 + if coin == _borrowed_token: + borrowed_ix = i + elif coin == _collateral_token: + collateral_ix = i + if collateral_ix == 100 or borrowed_ix == 100: + raise "Tokens not in pool" + price_oracle: IPriceOracle = IPriceOracle( + create_from_blueprint(POOL_PRICE_ORACLE_BLUEPRINT, _pool, N, borrowed_ix, collateral_ix) + ) + + res: address[3] = extcall FACTORY.create( + _borrowed_token, + _collateral_token, + _A, + _fee, + _loan_discount, + _liquidation_discount, + price_oracle, + _monetary_policy, + _name, + _supply_limit, + ) + + return res diff --git a/contracts/zaps/LeverageZap.vy b/contracts/zaps/LeverageZap.vy index 2330453d..ec64fc8d 100644 --- a/contracts/zaps/LeverageZap.vy +++ b/contracts/zaps/LeverageZap.vy @@ -268,24 +268,26 @@ def max_borrowable(controller: address, _user_collateral: uint256, _leverage_col @internal def _transferFrom(token: address, _from: address, _to: address, amount: uint256): + # TODO: use contracts.lib.token_lib.transferFrom if amount > 0: assert ERC20(token).transferFrom(_from, _to, amount, default_return_value=True) @internal def _approve(coin: address, spender: address): + # TODO: use contracts.lib.token_lib.max_approve if ERC20(coin).allowance(self, spender) == 0: assert ERC20(coin).approve(spender, max_value(uint256), default_return_value=True) @external @nonreentrant('lock') -def callback_deposit(user: address, stablecoins: uint256, user_collateral: uint256, d_debt: uint256, +def callback_deposit(user: address, borrowed: uint256, user_collateral: uint256, d_debt: uint256, callback_args: DynArray[uint256, 10], callback_bytes: Bytes[10**4] = b"") -> uint256[2]: """ @notice Callback method which should be called by controller to create leveraged position @param user Address of the user - @param stablecoins Always 0 + @param borrowed Always 0 @param user_collateral The amount of collateral token provided by user @param d_debt The amount to be borrowed (in addition to what has already been borrowed) @param callback_args [factory_id, controller_id, user_borrowed] @@ -316,12 +318,12 @@ def callback_deposit(user: address, stablecoins: uint256, user_collateral: uint2 @external @nonreentrant('lock') -def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, +def callback_repay(user: address, borrowed: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256,10], callback_bytes: Bytes[10 ** 4] = b"") -> uint256[2]: """ @notice Callback method which should be called by controller to create leveraged position @param user Address of the user - @param stablecoins The value from user_state + @param borrowed The value from user_state @param collateral The value from user_state @param debt The value from user_state @param callback_args [factory_id, controller_id, user_collateral, user_borrowed] diff --git a/contracts/zaps/PartialRepayZap.vy b/contracts/zaps/PartialRepayZap.vy new file mode 100644 index 00000000..b5f6d1da --- /dev/null +++ b/contracts/zaps/PartialRepayZap.vy @@ -0,0 +1,131 @@ +# pragma version 0.4.3 +# pragma nonreentrancy on + +""" +@title LlamaLendPartialRepayZap +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice Partially repays a position (self-liquidation) when health is low. + Liquidator provides borrowed tokens, receives withdrawn collateral. +""" + +from curve_std.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import IController +from contracts import Controller as ctrl +from curve_std import token as tkn +from contracts.interfaces import IPartialRepayZap as IZap +import contracts.lib.liquidation_lib as liq + +from contracts import constants as c + +implements: IZap + +# https://github.com/vyperlang/vyper/issues/4723 +WAD: constant(uint256) = c.WAD + +FRAC: public(immutable(uint256)) # fraction of position to repay (1e18 = 100%) +HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) + + +@deploy +def __init__( + _frac: uint256, # e.g. 5e16 == 5% + _health_threshold: int256, # e.g. 1e16 == 1% + ): + FRAC = _frac + HEALTH_THRESHOLD = _health_threshold + + +@internal +@view +def _x_down(_controller: IController, _user: address) -> uint256: + # Obtain the value of the users collateral if it + # was fully soft liquidated into borrowed tokens + return staticcall (staticcall _controller.amm()).get_x_down(_user) + + +@external +@view +def users_to_liquidate(_controller: IController, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[IZap.Position, 1000]: + """ + @notice Returns users eligible for partial self-liquidation through this zap. + @param _controller Address of the controller + @param _from Loan index to start iteration from + @param _limit Number of loans to inspect (0 = all) + @return Dynamic array with position info and zap-specific estimates + """ + # Cached only for readability purposes + CONTROLLER: IController = _controller + + base_positions: DynArray[IController.Position, 1000] = liq.users_with_health( + CONTROLLER, _from, _limit, HEALTH_THRESHOLD, True, self, False + ) + out: DynArray[IZap.Position, 1000] = [] + for i: uint256 in range(1000): + if i == len(base_positions): + break + pos: IController.Position = base_positions[i] + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(pos.user, FRAC) + x_down: uint256 = self._x_down(CONTROLLER, pos.user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), pos.debt) + out.append( + IZap.Position( + user=pos.user, + x=pos.x, + y=pos.y, + health=pos.health, + dx=unsafe_div(pos.y * ctrl._get_f_remove(FRAC, 0), WAD), + dy=unsafe_div(unsafe_mul(to_repay, ratio), WAD), + ) + ) + return out + + +@external +def liquidate_partial(_controller: IController, _user: address, _min_x: uint256): + """ + @notice Trigger partial self-liquidation of `user` using FRAC. + Caller supplies borrowed tokens; receives withdrawn collateral. + @param _controller Address of the controller + @param _user Address of the position owner (must have approved this zap in controller) + @param _min_x Minimal x withdrawn from AMM to guard against MEV + """ + # Cached only for readability purposes + CONTROLLER: IController = _controller + + BORROWED: IERC20 = staticcall CONTROLLER.borrowed_token() + COLLATERAL: IERC20 = staticcall CONTROLLER.collateral_token() + + assert staticcall CONTROLLER.approval(_user, self), "not approved" + assert staticcall CONTROLLER.health(_user, False) < HEALTH_THRESHOLD, "health too high" + + tkn.max_approve(BORROWED, CONTROLLER.address) + + total_debt: uint256 = staticcall CONTROLLER.debt(_user) + x_down: uint256 = self._x_down(CONTROLLER, _user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), total_debt) + + assert ratio > WAD, "position rekt" + + # Amount of borrowed token the liquidator must supply + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(_user, FRAC) + borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), WAD) + + tkn.transfer_from(BORROWED, msg.sender, self, borrowed_from_sender) + + extcall CONTROLLER.liquidate(_user, _min_x, FRAC) + collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) + tkn.transfer(COLLATERAL, msg.sender, collateral_received) + + # surplus amount goes into position repay + borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) + extcall CONTROLLER.repay(borrowed_amount, _user) + + log IZap.PartialRepay( + controller=_controller, + user=_user, + collateral_decrease=collateral_received, + borrowed_from_sender=borrowed_from_sender, + surplus_repaid=borrowed_amount, + ) diff --git a/contracts/zaps/PartialRepayZapCallback.vy b/contracts/zaps/PartialRepayZapCallback.vy new file mode 100644 index 00000000..f57a9693 --- /dev/null +++ b/contracts/zaps/PartialRepayZapCallback.vy @@ -0,0 +1,201 @@ +# pragma version 0.4.3 + +""" +@title LlamaLendPartialRepayZapCallback +@author Curve.Fi +@license Copyright (c) Curve.Fi, 2020-2025 - all rights reserved +@notice Partially repays a position (self-liquidation) when health is low, + using controller callback to forward assets directly to the caller. +""" + +from curve_std.interfaces import IERC20 +from contracts.interfaces import IAMM +from contracts.interfaces import IController +from contracts import Controller as ctrl +from curve_std import token as tkn +from contracts.interfaces import IPartialRepayZapCallback as IZap +import contracts.lib.liquidation_lib as liq + +from contracts import constants as c + +implements: IZap + +# https://github.com/vyperlang/vyper/issues/4723 +WAD: constant(uint256) = c.WAD + +FRAC: public(immutable(uint256)) # fraction of position to repay (1e18 = 100%) +HEALTH_THRESHOLD: public(immutable(int256)) # trigger threshold on controller.health(user, false) + +CALLDATA_MAX_SIZE: constant(uint256) = 10**4 +CALLBACK_SIGNATURE: constant(bytes4) = method_id("callback_liquidate_partial(bytes)",output_type=bytes4,) + + +@deploy +def __init__( + _frac: uint256, # e.g. 5e16 == 5% + _health_threshold: int256, # e.g. 1e16 == 1% + ): + FRAC = _frac + HEALTH_THRESHOLD = _health_threshold + + +@internal +@view +def _x_down(_controller: IController, _user: address) -> uint256: + # Obtain the value of the users collateral if it + # was fully soft liquidated into borrowed tokens + return staticcall (staticcall _controller.amm()).get_x_down(_user) + + +@external +@view +def users_to_liquidate(_controller: IController, _from: uint256 = 0, _limit: uint256 = 0) -> DynArray[IZap.Position, 1000]: + """ + @notice Returns users eligible for partial self-liquidation through this zap. + @param _controller Address of the controller + @param _from Loan index to start iteration from + @param _limit Number of loans to inspect (0 = all) + @return Dynamic array with position info and zap-specific estimates + """ + # Cached only for readability purposes + CONTROLLER: IController = _controller + + base_positions: DynArray[IController.Position, 1000] = liq.users_with_health( + CONTROLLER, _from, _limit, HEALTH_THRESHOLD, True, self, False + ) + out: DynArray[IZap.Position, 1000] = [] + for i: uint256 in range(1000): + if i == len(base_positions): + break + pos: IController.Position = base_positions[i] + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(pos.user, FRAC) + x_down: uint256 = self._x_down(CONTROLLER, pos.user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), pos.debt) + out.append( + IZap.Position( + user=pos.user, + x=pos.x, + y=pos.y, + health=pos.health, + dx=unsafe_div(pos.y * ctrl._get_f_remove(FRAC, 0), WAD), + dy=unsafe_div(unsafe_mul(to_repay, ratio), WAD), + ) + ) + return out + + +@external +def liquidate_partial( + _controller: IController, + _user: address, + _min_x: uint256, + _callbacker: address = empty(address), + _calldata: Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16] = b"", +): + """ + @notice Trigger partial self-liquidation of `user` using FRAC. + Caller supplies borrowed tokens; receives withdrawn collateral. + @param _controller Address of the controller + @param _user Address of the position owner (must have approved this zap in controller) + @param _min_x Minimal x withdrawn from AMM to guard against MEV + @param _callbacker Address of the callback contract + @param _calldata Any data for callbacker (address x 3 (64) + uint256 (32) + 2 * offset (32) + must be divided by 32 - slots (16)) + """ + # Cached only for readability purposes + CONTROLLER: IController = _controller + + BORROWED: IERC20 = staticcall CONTROLLER.borrowed_token() + COLLATERAL: IERC20 = staticcall CONTROLLER.collateral_token() + + assert staticcall CONTROLLER.approval(_user, self), "not approved" + assert staticcall CONTROLLER.health(_user, False) < HEALTH_THRESHOLD, "health too high" + + tkn.max_approve(BORROWED, CONTROLLER.address) + + total_debt: uint256 = staticcall CONTROLLER.debt(_user) + x_down: uint256 = self._x_down(CONTROLLER, _user) + ratio: uint256 = unsafe_div(unsafe_mul(x_down, WAD), total_debt) + + assert ratio > WAD, "position rekt" + + # Amount of borrowed token the liquidator must supply + to_repay: uint256 = staticcall CONTROLLER.tokens_to_liquidate(_user, FRAC) + borrowed_from_sender: uint256 = unsafe_div(unsafe_mul(to_repay, ratio), WAD) + + if _callbacker != empty(address): + liquidate_calldata: Bytes[CALLDATA_MAX_SIZE] = abi_encode(_controller.address, _user, borrowed_from_sender, _callbacker, _calldata) + extcall CONTROLLER.liquidate(_user, _min_x, FRAC, self, liquidate_calldata) + + else: + tkn.transfer_from(BORROWED, msg.sender, self, borrowed_from_sender) + + extcall CONTROLLER.liquidate(_user, _min_x, FRAC) + collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) + tkn.transfer(COLLATERAL, msg.sender, collateral_received) + + # surplus amount goes into position repay + borrowed_amount: uint256 = staticcall BORROWED.balanceOf(self) + extcall CONTROLLER.repay(borrowed_amount, _user) + + log IZap.PartialRepay( + controller=_controller, + user=_user, + surplus_repaid=borrowed_amount, + ) + + +@internal +def execute_callback( + callbacker: address, + callback_sig: bytes4, + calldata: Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16], +): + response: Bytes[64] = raw_call( + callbacker, + concat( + callback_sig, + abi_encode(calldata), + ), + max_outsize=64, + ) + + +@external +def callback_liquidate( + _user: address, + _borrowed: uint256, + _collateral: uint256, + _debt: uint256, + _calldata: Bytes[CALLDATA_MAX_SIZE], +) -> uint256[2]: + """ + @notice Controller callback invoked during liquidate. + @dev Provides borrowed tokens back to controller to cover shortfall and + forwards collateral to the liquidator via controller.transferFrom. + """ + controller: address = empty(address) + user: address = empty(address) + borrowed_from_sender: uint256 = 0 + callbacker: address = empty(address) + callbacker_calldata: Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16] = empty(Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16]) + + controller, user, borrowed_from_sender, callbacker, callbacker_calldata = abi_decode(_calldata, (address, address, uint256, address, Bytes[CALLDATA_MAX_SIZE - 32 * 6 - 16])) + assert msg.sender == controller, "wrong sender" + + # Cached only for readability purposes + CONTROLLER: IController = IController(controller) + BORROWED: IERC20 = staticcall CONTROLLER.borrowed_token() + COLLATERAL: IERC20 = staticcall CONTROLLER.collateral_token() + + collateral_received: uint256 = staticcall COLLATERAL.balanceOf(self) + tkn.transfer(COLLATERAL, callbacker, collateral_received) + + self.execute_callback( + callbacker, + CALLBACK_SIGNATURE, + callbacker_calldata + ) + + tkn.transfer_from(BORROWED, callbacker, self, borrowed_from_sender) + + return [borrowed_from_sender, 0] diff --git a/contracts/zaps/deprecated/DeleverageZap.vy b/contracts/zaps/deprecated/DeleverageZap.vy deleted file mode 100644 index 0813a3a3..00000000 --- a/contracts/zaps/deprecated/DeleverageZap.vy +++ /dev/null @@ -1,105 +0,0 @@ -# @version 0.3.10 - -""" -@title crvUSD de-leverage zap -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -""" - -interface ERC20: - def balanceOf(_for: address) -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - def decimals() -> uint256: view - -interface Router: - def exchange(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _expected: uint256, _pools: address[5]) -> uint256: payable - def get_dy(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _pools: address[5]) -> uint256: view - -interface Controller: - def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256: view - def user_state(user: address) -> uint256[4]: view - - -CRVUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E - -CONTROLLER: public(immutable(address)) -COLLATERAL: public(immutable(address)) -ROUTER: public(immutable(address)) - -routes: public(HashMap[uint256, address[11]]) -route_params: public(HashMap[uint256, uint256[5][5]]) -route_pools: public(HashMap[uint256, address[5]]) -route_names: public(HashMap[uint256, String[100]]) -routes_count: public(constant(uint256)) = 5 - - -@external -def __init__( - _controller: address, - _collateral: address, - _router: address, - _routes: DynArray[address[11], 5], - _route_params: DynArray[uint256[5][5], 5], - _route_pools: DynArray[address[5], 5], - _route_names: DynArray[String[100], 5], -): - CONTROLLER = _controller - COLLATERAL = _collateral - ROUTER = _router - - for i in range(5): - self.routes[i] = _routes[i] - self.route_params[i] = _route_params[i] - self.route_pools[i] = _route_pools[i] - self.route_names[i] = _route_names[i] - - ERC20(_collateral).approve(_router, max_value(uint256), default_return_value=True) - ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True) - ERC20(CRVUSD).approve(_controller, max_value(uint256), default_return_value=True) - - -@view -@external -def get_stablecoins(collateral: uint256, route_idx: uint256) -> uint256: - return Router(ROUTER).get_dy(self.routes[route_idx], self.route_params[route_idx], collateral, self.route_pools[route_idx]) - - -@external -@view -def calculate_debt_n1(collateral: uint256, route_idx: uint256, user: address) -> int256: - """ - @notice Calculate the upper band number after deleverage repay, which means that - collateral from user's position is converted to stablecoins to repay the debt. - @param collateral Amount of collateral (at its native precision). - @param route_idx Index of the route which should be use for exchange stablecoin to collateral. - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer. - """ - deleverage_collateral: uint256 = Router(ROUTER).get_dy(self.routes[route_idx], self.route_params[route_idx], collateral, self.route_pools[route_idx]) - state: uint256[4] = Controller(CONTROLLER).user_state(user) #collateral, stablecoin, debt, N - assert state[1] == 0, "Underwater, only full repayment is allowed" - assert deleverage_collateral < state[2], "Full repayment, position will be closed" - - return Controller(CONTROLLER).calculate_debt_n1(state[0] - collateral, state[2] - deleverage_collateral, state[3]) - - -@external -@nonreentrant('lock') -def callback_repay(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]: - """ - @notice Callback method which should be called by controller to repay by selling collateral - @param user Address of the user - @param stablecoins Amount of user's stablecoin in AMM - @param collateral Amount of user's collateral in AMM - @param debt Current debt amount - @param callback_args [route_idx, collateral_amount, min_recv] - return [deleverage_stablecoins, (collateral - collateral_amount)], deleverage_stablecoins is - the amount of stablecoins got as a result of selling collateral - """ - assert msg.sender == CONTROLLER - - route_idx: uint256 = callback_args[0] - collateral_amount: uint256 = callback_args[1] - min_recv: uint256 = callback_args[2] - deleverage_stablecoins: uint256 = Router(ROUTER).exchange(self.routes[route_idx], self.route_params[route_idx], collateral_amount, min_recv, self.route_pools[route_idx]) - - return [deleverage_stablecoins, ERC20(COLLATERAL).balanceOf(self)] diff --git a/contracts/zaps/deprecated/LeverageZap.vy b/contracts/zaps/deprecated/LeverageZap.vy deleted file mode 100644 index 88cb8205..00000000 --- a/contracts/zaps/deprecated/LeverageZap.vy +++ /dev/null @@ -1,329 +0,0 @@ -# @version 0.3.10 - -""" -@title crvUSD leverage zap -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Creates leverage on crvUSD via CurveRouter. Does calculations for leverage. -""" - -interface ERC20: - def balanceOf(_for: address) -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - def decimals() -> uint256: view - -interface Router: - def exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]) -> uint256: payable - def get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]) -> uint256: view - -interface Controller: - def loan_discount() -> uint256: view - def amm() -> address: view - def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256: view - -interface LLAMMA: - def A() -> uint256: view - def active_band() -> int256: view - def can_skip_bands(n_end: int256) -> bool: view - def get_base_price() -> uint256: view - def price_oracle() -> uint256: view - def p_oracle_up(n: int256) -> uint256: view - def active_band_with_skip() -> int256: view - - -DEAD_SHARES: constant(uint256) = 1000 -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_P_BASE_BANDS: constant(int256) = 5 -MAX_SKIP_TICKS: constant(uint256) = 1024 - -CRVUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E - -CONTROLLER: immutable(address) -ROUTER: immutable(Router) -AMM: immutable(LLAMMA) -A: immutable(uint256) -Aminus1: immutable(uint256) -LOG2_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) -COLLATERAL_PRECISION: immutable(uint256) - -routes: public(HashMap[uint256, address[9]]) -route_params: public(HashMap[uint256, uint256[3][4]]) -route_pools: public(HashMap[uint256, address[4]]) -route_names: public(HashMap[uint256, String[64]]) -routes_count: public(uint256) - - -@external -def __init__( - _controller: address, - _collateral: address, - _router: address, - _routes: DynArray[address[9], 20], - _route_params: DynArray[uint256[3][4], 20], - _route_pools: DynArray[address[4], 20], - _route_names: DynArray[String[64], 20], -): - CONTROLLER = _controller - ROUTER = Router(_router) - - amm: address = Controller(_controller).amm() - AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() - A = _A - Aminus1 = _A - 1 - LOG2_A_RATIO = self.log2(_A * 10 ** 18 / unsafe_sub(_A, 1)) - SQRT_BAND_RATIO = isqrt(unsafe_div(10 ** 36 * _A, unsafe_sub(_A, 1))) - COLLATERAL_PRECISION = pow_mod256(10, 18 - ERC20(_collateral).decimals()) - - for i in range(20): - if i >= len(_routes): - break - self.routes[i] = _routes[i] - self.route_params[i] = _route_params[i] - self.route_pools[i] = _route_pools[i] - self.route_names[i] = _route_names[i] - self.routes_count = len(_routes) - - ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True) - ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True) - - -@internal -@pure -def log2(_x: uint256) -> int256: - """ - @notice int(1e18 * log2(_x / 1e18)) - """ - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - # Might use more optimal solmate's log - inverse: bool = _x < 10**18 - res: uint256 = 0 - x: uint256 = _x - if inverse: - x = 10**36 / x - t: uint256 = 2**7 - for i in range(8): - p: uint256 = pow_mod256(2, t) - if x >= unsafe_mul(p, 10**18): - x = unsafe_div(x, p) - res = unsafe_add(unsafe_mul(t, 10**18), res) - t = unsafe_div(t, 2) - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res = unsafe_add(res, d) - x = unsafe_div(x, 2) - x = unsafe_div(unsafe_mul(x, x), 10**18) - d = unsafe_div(d, 2) - if inverse: - return -convert(res, int256) - else: - return convert(res, int256) - - -@internal -@view -def _get_k_effective(collateral: uint256, N: uint256) -> uint256: - """ - @notice Intermediary method which calculates k_effective defined as x_effective / p_base / y, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param N Number of bands the deposit is made into - @return k_effective - """ - # x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) = - # = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k) - # === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1) - # d_k_effective = N / sqrt(A / (A - 1)) - # d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N) - # Make some extra discount to always deposit lower when we have DEAD_SHARES rounding - discount: uint256 = Controller(CONTROLLER).loan_discount() - d_k_effective: uint256 = 10**18 * unsafe_sub( - 10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18) - ) / (SQRT_BAND_RATIO * N) - k_effective: uint256 = d_k_effective - for i in range(1, MAX_TICKS_UINT): - if i == N: - break - d_k_effective = unsafe_div(d_k_effective * Aminus1, A) - k_effective = unsafe_add(k_effective, d_k_effective) - return k_effective - - -@internal -@view -def _max_p_base() -> uint256: - """ - @notice Calculate max base price including skipping bands - """ - p_oracle: uint256 = AMM.price_oracle() - # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands - n1: int256 = unsafe_div(self.log2(AMM.get_base_price() * 10**18 / p_oracle), LOG2_A_RATIO) + MAX_P_BASE_BANDS - p_base: uint256 = AMM.p_oracle_up(n1) - n_min: int256 = AMM.active_band_with_skip() - - for i in range(MAX_SKIP_TICKS + 1): - n1 -= 1 - if n1 <= n_min: - break - p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - -@view -@internal -def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - - -@view -@internal -def _get_collateral_and_avg_price(stablecoin: uint256, route_idx: uint256) -> uint256[2]: - collateral: uint256 = self._get_collateral(stablecoin, route_idx) - return [collateral, stablecoin * 10**18 / (collateral * COLLATERAL_PRECISION)] - - -@view -@external -@nonreentrant('lock') -def get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of collateral by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of collateral - """ - return self._get_collateral(stablecoin, route_idx) - - -@view -@external -@nonreentrant('lock') -def get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice This method is needed just to make ABI the same as ABI for sfrxETH and wstETH - """ - return self._get_collateral(stablecoin, route_idx) - - -@external -@view -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt with full leverage, which means that all borrowed - stablecoin is converted to collateral coin and deposited in addition - to collateral provided by user. Reverts if requested debt is too high. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - leverage_collateral: uint256 = self._get_collateral(debt, route_idx) - return Controller(CONTROLLER).calculate_debt_n1(collateral + leverage_collateral, debt, N) - - -@internal -@view -def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - # max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg) - user_collateral: uint256 = collateral * COLLATERAL_PRECISION - leverage_collateral: uint256 = 0 - k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N) - max_p_base: uint256 = self._max_p_base() - p_avg: uint256 = AMM.price_oracle() - max_borrowable_prev: uint256 = 0 - max_borrowable: uint256 = 0 - for i in range(10): - max_borrowable_prev = max_borrowable - max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg) - if max_borrowable > max_borrowable_prev: - if max_borrowable - max_borrowable_prev <= 1: - return max_borrowable - else: - if max_borrowable_prev - max_borrowable <= 1: - return max_borrowable - res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx) - leverage_collateral = res[0] - p_avg = res[1] - k_effective = self._get_k_effective(user_collateral + leverage_collateral, N) - - return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has - - -@external -@view -def max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - return self._max_borrowable(collateral, N ,route_idx) - - -@external -@view -def max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum collateral position which can be created with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return user_collateral + max_leverage_collateral - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return collateral + max_leverage_collateral - - -@external -@view -def max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]: - """ - @notice Calculation of maximum which can be borrowed with leverage and maximum collateral position which can be created then - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return [max_borrowable, user_collateral + max_leverage_collateral] - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return [max_borrowable, collateral + max_leverage_collateral] - - -@external -@nonreentrant('lock') -def callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]: - """ - @notice Callback method which should be called by controller to create leveraged position - @param user Address of the user - @param stablecoins Amount of stablecoin (always = 0) - @param collateral Amount of collateral given by user - @param debt Borrowed amount - @param callback_args [route_idx, min_recv] - return [0, leverage_collateral], leverage_collateral is the amount of collateral got as a result of selling borrowed stablecoin - """ - assert msg.sender == CONTROLLER - - route_idx: uint256 = callback_args[0] - min_recv: uint256 = callback_args[1] - leverage_collateral: uint256 = ROUTER.exchange_multiple(self.routes[route_idx], self.route_params[route_idx], debt, min_recv, self.route_pools[route_idx]) - - return [0, leverage_collateral] diff --git a/contracts/zaps/deprecated/LeverageZapNewRouter.vy b/contracts/zaps/deprecated/LeverageZapNewRouter.vy deleted file mode 100644 index 957680b8..00000000 --- a/contracts/zaps/deprecated/LeverageZapNewRouter.vy +++ /dev/null @@ -1,328 +0,0 @@ -# @version 0.3.10 - -""" -@title crvUSD leverage zap -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Creates leverage on crvUSD via CurveRouter. Does calculations for leverage. -""" - -interface ERC20: - def balanceOf(_for: address) -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - def decimals() -> uint256: view - -interface Router: - def exchange(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _expected: uint256, _pools: address[5]) -> uint256: payable - def get_dy(_route: address[11], _swap_params: uint256[5][5], _amount: uint256, _pools: address[5]) -> uint256: view - -interface Controller: - def loan_discount() -> uint256: view - def amm() -> address: view - def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256: view - -interface LLAMMA: - def A() -> uint256: view - def active_band() -> int256: view - def can_skip_bands(n_end: int256) -> bool: view - def get_base_price() -> uint256: view - def price_oracle() -> uint256: view - def p_oracle_up(n: int256) -> uint256: view - def active_band_with_skip() -> int256: view - - -DEAD_SHARES: constant(uint256) = 1000 -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_P_BASE_BANDS: constant(int256) = 5 -MAX_SKIP_TICKS: constant(uint256) = 1024 - -CRVUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E - -CONTROLLER: immutable(address) -ROUTER: immutable(Router) -AMM: immutable(LLAMMA) -A: immutable(uint256) -Aminus1: immutable(uint256) -LOG2_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) -COLLATERAL_PRECISION: immutable(uint256) - -routes: public(HashMap[uint256, address[11]]) -route_params: public(HashMap[uint256, uint256[5][5]]) -route_pools: public(HashMap[uint256, address[5]]) -route_names: public(HashMap[uint256, String[100]]) -routes_count: public(constant(uint256)) = 5 - - -@external -def __init__( - _controller: address, - _collateral: address, - _router: address, - _routes: DynArray[address[11], 5], - _route_params: DynArray[uint256[5][5], 5], - _route_pools: DynArray[address[5], 5], - _route_names: DynArray[String[100], 5], -): - CONTROLLER = _controller - ROUTER = Router(_router) - - amm: address = Controller(_controller).amm() - AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() - A = _A - Aminus1 = _A - 1 - LOG2_A_RATIO = self.log2(_A * 10 ** 18 / unsafe_sub(_A, 1)) - SQRT_BAND_RATIO = isqrt(unsafe_div(10 ** 36 * _A, unsafe_sub(_A, 1))) - COLLATERAL_PRECISION = pow_mod256(10, 18 - ERC20(_collateral).decimals()) - - for i in range(5): - if i >= len(_routes): - break - self.routes[i] = _routes[i] - self.route_params[i] = _route_params[i] - self.route_pools[i] = _route_pools[i] - self.route_names[i] = _route_names[i] - - ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True) - ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True) - - -@internal -@pure -def log2(_x: uint256) -> int256: - """ - @notice int(1e18 * log2(_x / 1e18)) - """ - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - # Might use more optimal solmate's log - inverse: bool = _x < 10**18 - res: uint256 = 0 - x: uint256 = _x - if inverse: - x = 10**36 / x - t: uint256 = 2**7 - for i in range(8): - p: uint256 = pow_mod256(2, t) - if x >= unsafe_mul(p, 10**18): - x = unsafe_div(x, p) - res = unsafe_add(unsafe_mul(t, 10**18), res) - t = unsafe_div(t, 2) - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res = unsafe_add(res, d) - x = unsafe_div(x, 2) - x = unsafe_div(unsafe_mul(x, x), 10**18) - d = unsafe_div(d, 2) - if inverse: - return -convert(res, int256) - else: - return convert(res, int256) - - -@internal -@view -def _get_k_effective(collateral: uint256, N: uint256) -> uint256: - """ - @notice Intermediary method which calculates k_effective defined as x_effective / p_base / y, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param N Number of bands the deposit is made into - @return k_effective - """ - # x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) = - # = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k) - # === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1) - # d_k_effective = 1 / N / sqrt(A / (A - 1)) - # d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N) - # Make some extra discount to always deposit lower when we have DEAD_SHARES rounding - discount: uint256 = Controller(CONTROLLER).loan_discount() - d_k_effective: uint256 = 10**18 * unsafe_sub( - 10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18) - ) / (SQRT_BAND_RATIO * N) - k_effective: uint256 = d_k_effective - for i in range(1, MAX_TICKS_UINT): - if i == N: - break - d_k_effective = unsafe_div(d_k_effective * Aminus1, A) - k_effective = unsafe_add(k_effective, d_k_effective) - return k_effective - - -@internal -@view -def _max_p_base() -> uint256: - """ - @notice Calculate max base price including skipping bands - """ - p_oracle: uint256 = AMM.price_oracle() - # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands - n1: int256 = unsafe_div(self.log2(AMM.get_base_price() * 10**18 / p_oracle), LOG2_A_RATIO) + MAX_P_BASE_BANDS - p_base: uint256 = AMM.p_oracle_up(n1) - n_min: int256 = AMM.active_band_with_skip() - - for i in range(MAX_SKIP_TICKS + 1): - n1 -= 1 - if n1 <= n_min: - break - p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - -@view -@internal -def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - return ROUTER.get_dy(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - - -@view -@internal -def _get_collateral_and_avg_price(stablecoin: uint256, route_idx: uint256) -> uint256[2]: - collateral: uint256 = self._get_collateral(stablecoin, route_idx) - return [collateral, stablecoin * 10**18 / (collateral * COLLATERAL_PRECISION)] - - -@view -@external -@nonreentrant('lock') -def get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of collateral by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of collateral - """ - return self._get_collateral(stablecoin, route_idx) - - -@view -@external -@nonreentrant('lock') -def get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice This method is needed just to make ABI the same as ABI for sfrxETH and wstETH - """ - return self._get_collateral(stablecoin, route_idx) - - -@external -@view -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt with full leverage, which means that all borrowed - stablecoin is converted to collateral coin and deposited in addition - to collateral provided by user. Reverts if requested debt is too high. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - leverage_collateral: uint256 = self._get_collateral(debt, route_idx) - return Controller(CONTROLLER).calculate_debt_n1(collateral + leverage_collateral, debt, N) - - -@internal -@view -def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - # max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg) - user_collateral: uint256 = collateral * COLLATERAL_PRECISION - leverage_collateral: uint256 = 0 - k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N) - max_p_base: uint256 = self._max_p_base() - p_avg: uint256 = AMM.price_oracle() - max_borrowable_prev: uint256 = 0 - max_borrowable: uint256 = 0 - for i in range(10): - max_borrowable_prev = max_borrowable - max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg) - if max_borrowable > max_borrowable_prev: - if max_borrowable - max_borrowable_prev <= 1: - return max_borrowable - else: - if max_borrowable_prev - max_borrowable <= 1: - return max_borrowable - res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx) - leverage_collateral = res[0] - p_avg = res[1] - k_effective = self._get_k_effective(user_collateral + leverage_collateral, N) - - return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has - - -@external -@view -def max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - return self._max_borrowable(collateral, N ,route_idx) - - -@external -@view -def max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum collateral position which can be created with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return user_collateral + max_leverage_collateral - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return collateral + max_leverage_collateral - - -@external -@view -def max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]: - """ - @notice Calculation of maximum which can be borrowed with leverage and maximum collateral position which can be created then - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return [max_borrowable, user_collateral + max_leverage_collateral] - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return [max_borrowable, collateral + max_leverage_collateral] - - -@external -@nonreentrant('lock') -def callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]: - """ - @notice Callback method which should be called by controller to create leveraged position - @param user Address of the user - @param stablecoins Amount of stablecoin (always = 0) - @param collateral Amount of collateral given by user - @param debt Borrowed amount - @param callback_args [route_idx, min_recv] - return [0, leverage_collateral], leverage_collateral is the amount of collateral got as a result of selling borrowed stablecoin - """ - assert msg.sender == CONTROLLER - - route_idx: uint256 = callback_args[0] - min_recv: uint256 = callback_args[1] - leverage_collateral: uint256 = ROUTER.exchange(self.routes[route_idx], self.route_params[route_idx], debt, min_recv, self.route_pools[route_idx]) - - return [0, leverage_collateral] diff --git a/contracts/zaps/deprecated/LeverageZapSfrxETH.vy b/contracts/zaps/deprecated/LeverageZapSfrxETH.vy deleted file mode 100644 index 7f04ca1b..00000000 --- a/contracts/zaps/deprecated/LeverageZapSfrxETH.vy +++ /dev/null @@ -1,341 +0,0 @@ -# @version 0.3.10 - -""" -@title sfrxETH crvUSD leverage zap -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Creates leverage on crvUSD via CurveRouter. Does calculations for leverage. -""" - -interface ERC20: - def balanceOf(_for: address) -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - -interface Router: - def exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]) -> uint256: payable - def get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]) -> uint256: view - -interface Controller: - def loan_discount() -> uint256: view - def amm() -> address: view - def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256: view - -interface LLAMMA: - def A() -> uint256: view - def active_band() -> int256: view - def can_skip_bands(n_end: int256) -> bool: view - def get_base_price() -> uint256: view - def price_oracle() -> uint256: view - def p_oracle_up(n: int256) -> uint256: view - def active_band_with_skip() -> int256: view - -interface ISFRXETH: - def convertToShares(assets: uint256) -> uint256: view - def deposit(assets: uint256, receiver: address) -> uint256: nonpayable - - -DEAD_SHARES: constant(uint256) = 1000 -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_P_BASE_BANDS: constant(int256) = 5 -MAX_SKIP_TICKS: constant(uint256) = 1024 - -CRVUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E -FRXETH: constant(address) = 0x5E8422345238F34275888049021821E8E08CAa1f - -CONTROLLER: immutable(address) -SFRXETH: immutable(ISFRXETH) -ROUTER: immutable(Router) -AMM: immutable(LLAMMA) -A: immutable(uint256) -Aminus1: immutable(uint256) -LOG2_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) -COLLATERAL_PRECISION: immutable(uint256) - -routes: public(HashMap[uint256, address[9]]) -route_params: public(HashMap[uint256, uint256[3][4]]) -route_pools: public(HashMap[uint256, address[4]]) -route_names: public(HashMap[uint256, String[64]]) -routes_count: public(uint256) - - -@external -def __init__( - _controller: address, - _collateral: address, - _router: address, - _routes: DynArray[address[9], 20], - _route_params: DynArray[uint256[3][4], 20], - _route_pools: DynArray[address[4], 20], - _route_names: DynArray[String[64], 20], -): - CONTROLLER = _controller - SFRXETH = ISFRXETH(_collateral) - ROUTER = Router(_router) - - amm: address = Controller(_controller).amm() - AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() - A = _A - Aminus1 = _A - 1 - LOG2_A_RATIO = self.log2(_A * 10 ** 18 / unsafe_sub(_A, 1)) - SQRT_BAND_RATIO = isqrt(unsafe_div(10 ** 36 * _A, unsafe_sub(_A, 1))) - COLLATERAL_PRECISION = 1 - - for i in range(20): - if i >= len(_routes): - break - self.routes[i] = _routes[i] - self.route_params[i] = _route_params[i] - self.route_pools[i] = _route_pools[i] - self.route_names[i] = _route_names[i] - self.routes_count = len(_routes) - - ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True) - ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True) - ERC20(FRXETH).approve(SFRXETH.address, max_value(uint256), default_return_value=True) - - -@internal -@pure -def log2(_x: uint256) -> int256: - """ - @notice int(1e18 * log2(_x / 1e18)) - """ - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - # Might use more optimal solmate's log - inverse: bool = _x < 10**18 - res: uint256 = 0 - x: uint256 = _x - if inverse: - x = 10**36 / x - t: uint256 = 2**7 - for i in range(8): - p: uint256 = pow_mod256(2, t) - if x >= unsafe_mul(p, 10**18): - x = unsafe_div(x, p) - res = unsafe_add(unsafe_mul(t, 10**18), res) - t = unsafe_div(t, 2) - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res = unsafe_add(res, d) - x = unsafe_div(x, 2) - x = unsafe_div(unsafe_mul(x, x), 10**18) - d = unsafe_div(d, 2) - if inverse: - return -convert(res, int256) - else: - return convert(res, int256) - - -@internal -@view -def _get_k_effective(collateral: uint256, N: uint256) -> uint256: - """ - @notice Intermediary method which calculates k_effective defined as x_effective / p_base / y, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param N Number of bands the deposit is made into - @return k_effective - """ - # x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) = - # = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k) - # === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1) - # d_k_effective = N / sqrt(A / (A - 1)) - # d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N) - # Make some extra discount to always deposit lower when we have DEAD_SHARES rounding - discount: uint256 = Controller(CONTROLLER).loan_discount() - d_k_effective: uint256 = 10**18 * unsafe_sub( - 10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18) - ) / (SQRT_BAND_RATIO * N) - k_effective: uint256 = d_k_effective - for i in range(1, MAX_TICKS_UINT): - if i == N: - break - d_k_effective = unsafe_div(d_k_effective * Aminus1, A) - k_effective = unsafe_add(k_effective, d_k_effective) - return k_effective - - -@internal -@view -def _max_p_base() -> uint256: - """ - @notice Calculate max base price including skipping bands - """ - p_oracle: uint256 = AMM.price_oracle() - # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands - n1: int256 = unsafe_div(self.log2(AMM.get_base_price() * 10**18 / p_oracle), LOG2_A_RATIO) + MAX_P_BASE_BANDS - p_base: uint256 = AMM.p_oracle_up(n1) - n_min: int256 = AMM.active_band_with_skip() - - for i in range(MAX_SKIP_TICKS + 1): - n1 -= 1 - if n1 <= n_min: - break - p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - -@view -@internal -def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - frxeth_amt: uint256 = ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - return SFRXETH.convertToShares(frxeth_amt) - - -@view -@internal -def _get_collateral_and_avg_price(stablecoin: uint256, route_idx: uint256) -> uint256[2]: - collateral: uint256 = self._get_collateral(stablecoin, route_idx) - return [collateral, stablecoin * 10**18 / (collateral * COLLATERAL_PRECISION)] - - -@view -@external -@nonreentrant('lock') -def get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of sfrxETH by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of sfrxETH - """ - return self._get_collateral(stablecoin, route_idx) - - -@view -@external -@nonreentrant('lock') -def get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of frxETH by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of frxETH - """ - return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - - -@external -@view -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt with full leverage, which means that all borrowed - stablecoin is converted to collateral coin and deposited in addition - to collateral provided by user. Reverts if requested debt is too high. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - leverage_collateral: uint256 = self._get_collateral(debt, route_idx) - return Controller(CONTROLLER).calculate_debt_n1(collateral + leverage_collateral, debt, N) - - -@internal -@view -def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - # max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg) - user_collateral: uint256 = collateral * COLLATERAL_PRECISION - leverage_collateral: uint256 = 0 - k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N) - max_p_base: uint256 = self._max_p_base() - p_avg: uint256 = AMM.price_oracle() - max_borrowable_prev: uint256 = 0 - max_borrowable: uint256 = 0 - for i in range(10): - max_borrowable_prev = max_borrowable - max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg) - if max_borrowable > max_borrowable_prev: - if max_borrowable - max_borrowable_prev <= 1: - return max_borrowable - else: - if max_borrowable_prev - max_borrowable <= 1: - return max_borrowable - res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx) - leverage_collateral = res[0] - p_avg = res[1] - k_effective = self._get_k_effective(user_collateral + leverage_collateral, N) - - return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has - - -@external -@view -def max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - return self._max_borrowable(collateral, N ,route_idx) - - -@external -@view -def max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum collateral position which can be created with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return user_collateral + max_leverage_collateral - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return collateral + max_leverage_collateral - - -@external -@view -def max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]: - """ - @notice Calculation of maximum which can be borrowed with leverage and maximum collateral position which can be created then - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return [max_borrowable, user_collateral + max_leverage_collateral] - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return [max_borrowable, collateral + max_leverage_collateral] - - -@external -@nonreentrant('lock') -def callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]: - """ - @notice Callback method which should be called by controller to create leveraged position - @param user Address of the user - @param stablecoins Amount of stablecoin (always = 0) - @param collateral Amount of collateral given by user - @param debt Borrowed amount - @param callback_args [route_idx, min_recv] - return [0, leverage_collateral], leverage_collateral is the amount of collateral got as a result of selling borrowed stablecoin - """ - assert msg.sender == CONTROLLER - - route_idx: uint256 = callback_args[0] - min_recv: uint256 = callback_args[1] - frxeth: uint256 = ROUTER.exchange_multiple(self.routes[route_idx], self.route_params[route_idx], debt, min_recv, self.route_pools[route_idx]) - leverage_collateral: uint256 = SFRXETH.deposit(frxeth, self) - - return [0, leverage_collateral] diff --git a/contracts/zaps/deprecated/LeverageZapWstETH.vy b/contracts/zaps/deprecated/LeverageZapWstETH.vy deleted file mode 100644 index ff6dcab4..00000000 --- a/contracts/zaps/deprecated/LeverageZapWstETH.vy +++ /dev/null @@ -1,341 +0,0 @@ -# @version 0.3.10 - -""" -@title wstETH crvUSD leverage zap -@author Curve.Fi -@license Copyright (c) Curve.Fi, 2020-2023 - all rights reserved -@notice Creates leverage on crvUSD via CurveRouter. Does calculations for leverage. -""" - -interface ERC20: - def balanceOf(_for: address) -> uint256: view - def approve(_spender: address, _value: uint256) -> bool: nonpayable - -interface Router: - def exchange_multiple(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _expected: uint256, _pools: address[4]) -> uint256: payable - def get_exchange_multiple_amount(_route: address[9], _swap_params: uint256[3][4], _amount: uint256, _pools: address[4]) -> uint256: view - -interface Controller: - def loan_discount() -> uint256: view - def amm() -> address: view - def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256) -> int256: view - -interface LLAMMA: - def A() -> uint256: view - def active_band() -> int256: view - def can_skip_bands(n_end: int256) -> bool: view - def get_base_price() -> uint256: view - def price_oracle() -> uint256: view - def p_oracle_up(n: int256) -> uint256: view - def active_band_with_skip() -> int256: view - -interface IWSTETH: - def getWstETHByStETH(_stETHAmount: uint256) -> uint256: view - def wrap(_stETHAmount: uint256) -> uint256: nonpayable - - -DEAD_SHARES: constant(uint256) = 1000 -MAX_TICKS_UINT: constant(uint256) = 50 -MAX_P_BASE_BANDS: constant(int256) = 5 -MAX_SKIP_TICKS: constant(uint256) = 1024 - -CRVUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E -STETH: constant(address) = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 - -CONTROLLER: immutable(address) -WSTETH: immutable(IWSTETH) -ROUTER: immutable(Router) -AMM: immutable(LLAMMA) -A: immutable(uint256) -Aminus1: immutable(uint256) -LOG2_A_RATIO: immutable(int256) # log(A / (A - 1)) -SQRT_BAND_RATIO: immutable(uint256) -COLLATERAL_PRECISION: immutable(uint256) - -routes: public(HashMap[uint256, address[9]]) -route_params: public(HashMap[uint256, uint256[3][4]]) -route_pools: public(HashMap[uint256, address[4]]) -route_names: public(HashMap[uint256, String[64]]) -routes_count: public(uint256) - - -@external -def __init__( - _controller: address, - _collateral: address, - _router: address, - _routes: DynArray[address[9], 20], - _route_params: DynArray[uint256[3][4], 20], - _route_pools: DynArray[address[4], 20], - _route_names: DynArray[String[64], 20], -): - CONTROLLER = _controller - WSTETH = IWSTETH(_collateral) - ROUTER = Router(_router) - - amm: address = Controller(_controller).amm() - AMM = LLAMMA(amm) - _A: uint256 = LLAMMA(amm).A() - A = _A - Aminus1 = _A - 1 - LOG2_A_RATIO = self.log2(_A * 10 ** 18 / unsafe_sub(_A, 1)) - SQRT_BAND_RATIO = isqrt(unsafe_div(10 ** 36 * _A, unsafe_sub(_A, 1))) - COLLATERAL_PRECISION = 1 - - for i in range(20): - if i >= len(_routes): - break - self.routes[i] = _routes[i] - self.route_params[i] = _route_params[i] - self.route_pools[i] = _route_pools[i] - self.route_names[i] = _route_names[i] - self.routes_count = len(_routes) - - ERC20(CRVUSD).approve(_router, max_value(uint256), default_return_value=True) - ERC20(_collateral).approve(_controller, max_value(uint256), default_return_value=True) - ERC20(STETH).approve(WSTETH.address, max_value(uint256), default_return_value=True) - - -@internal -@pure -def log2(_x: uint256) -> int256: - """ - @notice int(1e18 * log2(_x / 1e18)) - """ - # adapted from: https://medium.com/coinmonks/9aef8515136e - # and vyper log implementation - # Might use more optimal solmate's log - inverse: bool = _x < 10**18 - res: uint256 = 0 - x: uint256 = _x - if inverse: - x = 10**36 / x - t: uint256 = 2**7 - for i in range(8): - p: uint256 = pow_mod256(2, t) - if x >= unsafe_mul(p, 10**18): - x = unsafe_div(x, p) - res = unsafe_add(unsafe_mul(t, 10**18), res) - t = unsafe_div(t, 2) - d: uint256 = 10**18 - for i in range(34): # 10 decimals: math.log(10**10, 2) == 33.2. Need more? - if (x >= 2 * 10**18): - res = unsafe_add(res, d) - x = unsafe_div(x, 2) - x = unsafe_div(unsafe_mul(x, x), 10**18) - d = unsafe_div(d, 2) - if inverse: - return -convert(res, int256) - else: - return convert(res, int256) - - -@internal -@view -def _get_k_effective(collateral: uint256, N: uint256) -> uint256: - """ - @notice Intermediary method which calculates k_effective defined as x_effective / p_base / y, - however discounted by loan_discount. - x_effective is an amount which can be obtained from collateral when liquidating - @param N Number of bands the deposit is made into - @return k_effective - """ - # x_effective = sum_{i=0..N-1}(y / N * p(n_{n1+i})) = - # = y / N * p_oracle_up(n1) * sqrt((A - 1) / A) * sum_{0..N-1}(((A-1) / A)**k) - # === d_y_effective * p_oracle_up(n1) * sum(...) === y * k_effective * p_oracle_up(n1) - # d_k_effective = N / sqrt(A / (A - 1)) - # d_k_effective: uint256 = 10**18 * unsafe_sub(10**18, discount) / (SQRT_BAND_RATIO * N) - # Make some extra discount to always deposit lower when we have DEAD_SHARES rounding - discount: uint256 = Controller(CONTROLLER).loan_discount() - d_k_effective: uint256 = 10**18 * unsafe_sub( - 10**18, min(discount + (DEAD_SHARES * 10**18) / max(collateral / N, DEAD_SHARES), 10**18) - ) / (SQRT_BAND_RATIO * N) - k_effective: uint256 = d_k_effective - for i in range(1, MAX_TICKS_UINT): - if i == N: - break - d_k_effective = unsafe_div(d_k_effective * Aminus1, A) - k_effective = unsafe_add(k_effective, d_k_effective) - return k_effective - - -@internal -@view -def _max_p_base() -> uint256: - """ - @notice Calculate max base price including skipping bands - """ - p_oracle: uint256 = AMM.price_oracle() - # Should be correct unless price changes suddenly by MAX_P_BASE_BANDS+ bands - n1: int256 = unsafe_div(self.log2(AMM.get_base_price() * 10**18 / p_oracle), LOG2_A_RATIO) + MAX_P_BASE_BANDS - p_base: uint256 = AMM.p_oracle_up(n1) - n_min: int256 = AMM.active_band_with_skip() - - for i in range(MAX_SKIP_TICKS + 1): - n1 -= 1 - if n1 <= n_min: - break - p_base_prev: uint256 = p_base - p_base = unsafe_div(p_base * A, Aminus1) - if p_base > p_oracle: - return p_base_prev - - return p_base - - -@view -@internal -def _get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - steth_amt: uint256 = ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - return WSTETH.getWstETHByStETH(steth_amt) - - -@view -@internal -def _get_collateral_and_avg_price(stablecoin: uint256, route_idx: uint256) -> uint256[2]: - collateral: uint256 = self._get_collateral(stablecoin, route_idx) - return [collateral, stablecoin * 10**18 / (collateral * COLLATERAL_PRECISION)] - - -@view -@external -@nonreentrant('lock') -def get_collateral(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of wstETH by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of wstETH - """ - return self._get_collateral(stablecoin, route_idx) - - -@view -@external -@nonreentrant('lock') -def get_collateral_underlying(stablecoin: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculate the expected amount of stETH by given stablecoin amount - @param stablecoin Amount of stablecoin - @param route_idx Index of the route to use - @return Amount of stETH - """ - return ROUTER.get_exchange_multiple_amount(self.routes[route_idx], self.route_params[route_idx], stablecoin, self.route_pools[route_idx]) - - -@external -@view -def calculate_debt_n1(collateral: uint256, debt: uint256, N: uint256, route_idx: uint256) -> int256: - """ - @notice Calculate the upper band number for the deposit to sit in to support - the given debt with full leverage, which means that all borrowed - stablecoin is converted to collateral coin and deposited in addition - to collateral provided by user. Reverts if requested debt is too high. - @param collateral Amount of collateral (at its native precision) - @param debt Amount of requested debt - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Upper band n1 (n1 <= n2) to deposit into. Signed integer - """ - leverage_collateral: uint256 = self._get_collateral(debt, route_idx) - return Controller(CONTROLLER).calculate_debt_n1(collateral + leverage_collateral, debt, N) - - -@internal -@view -def _max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - # max_borrowable = collateral / (1 / (k_effective * max_p_base) - 1 / p_avg) - user_collateral: uint256 = collateral * COLLATERAL_PRECISION - leverage_collateral: uint256 = 0 - k_effective: uint256 = self._get_k_effective(user_collateral + leverage_collateral, N) - max_p_base: uint256 = self._max_p_base() - p_avg: uint256 = AMM.price_oracle() - max_borrowable_prev: uint256 = 0 - max_borrowable: uint256 = 0 - for i in range(10): - max_borrowable_prev = max_borrowable - max_borrowable = user_collateral * 10**18 / (10**36 / k_effective * 10**18 / max_p_base - 10**36 / p_avg) - if max_borrowable > max_borrowable_prev: - if max_borrowable - max_borrowable_prev <= 1: - return max_borrowable - else: - if max_borrowable_prev - max_borrowable <= 1: - return max_borrowable - res: uint256[2] = self._get_collateral_and_avg_price(max_borrowable, route_idx) - leverage_collateral = res[0] - p_avg = res[1] - k_effective = self._get_k_effective(user_collateral + leverage_collateral, N) - - return min(max_borrowable * 999 / 1000, ERC20(CRVUSD).balanceOf(CONTROLLER)) # Cannot borrow beyond the amount of coins Controller has - - -@external -@view -def max_borrowable(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum which can be borrowed with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return Maximum amount of stablecoin to borrow with leverage - """ - return self._max_borrowable(collateral, N ,route_idx) - - -@external -@view -def max_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256: - """ - @notice Calculation of maximum collateral position which can be created with leverage - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return user_collateral + max_leverage_collateral - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return collateral + max_leverage_collateral - - -@external -@view -def max_borrowable_and_collateral(collateral: uint256, N: uint256, route_idx: uint256) -> uint256[2]: - """ - @notice Calculation of maximum which can be borrowed with leverage and maximum collateral position which can be created then - @param collateral Amount of collateral (at its native precision) - @param N Number of bands to deposit into - @param route_idx Index of the route which should be use for exchange stablecoin to collateral - @return [max_borrowable, user_collateral + max_leverage_collateral] - """ - max_borrowable: uint256 = self._max_borrowable(collateral, N, route_idx) - max_leverage_collateral: uint256 = self._get_collateral(max_borrowable, route_idx) - return [max_borrowable, collateral + max_leverage_collateral] - - -@external -@nonreentrant('lock') -def callback_deposit(user: address, stablecoins: uint256, collateral: uint256, debt: uint256, callback_args: DynArray[uint256, 5]) -> uint256[2]: - """ - @notice Callback method which should be called by controller to create leveraged position - @param user Address of the user - @param stablecoins Amount of stablecoin (always = 0) - @param collateral Amount of collateral given by user - @param debt Borrowed amount - @param callback_args [route_idx, min_recv] - return [0, leverage_collateral], leverage_collateral is the amount of collateral got as a result of selling borrowed stablecoin - """ - assert msg.sender == CONTROLLER - - route_idx: uint256 = callback_args[0] - min_recv: uint256 = callback_args[1] - steth: uint256 = ROUTER.exchange_multiple(self.routes[route_idx], self.route_params[route_idx], debt, min_recv, self.route_pools[route_idx]) - leverage_collateral: uint256 = WSTETH.wrap(steth) - - return [0, leverage_collateral] diff --git a/deployment-logs/fraxtal-lending.log b/deployment-logs/fraxtal-lending.log index c8ea2c0e..f45de8a0 100644 --- a/deployment-logs/fraxtal-lending.log +++ b/deployment-logs/fraxtal-lending.log @@ -1,5 +1,5 @@ $ python3 scripts/deploy-lending-fraxtal.py --markets -Password: +Password: tx broadcasted: 0xee4792ea06ed20e60e6a34ac549b2693cb012522a039e132ae83c1f12a69018d /usr/lib/pypy3.10/functools.py:993: UserWarning: debug_traceTransaction not available! titanoboa will try hard to interact with the network, but this means that titanoboa is not able to do certain safety checks at runtime. it is recommended to switch to a node or provider with debug_traceTransaction. val = self.func(instance) diff --git a/deployment-logs/mainnet-attempt-4.log b/deployment-logs/mainnet-attempt-4.log index a7845cef..f8eaa5bd 100644 --- a/deployment-logs/mainnet-attempt-4.log +++ b/deployment-logs/mainnet-attempt-4.log @@ -238,4 +238,3 @@ Owner proxy: 0x01c6808Eb242c826d32f03712D66D5E613782363 Price Aggregator: 0xCb70bbAEC89b577617F835E7a2e126dA9e5aCF21 PegKeepers: ['0xb8A3f8E783D52CfB9E632276714234661dB698e6', '0x8AeB58603eFB7a9F63712A2506df01b685ba1c4C', '0x89AC9A0B48fc66875De710aB7EE53027970064DC', '0xE38dAA41bE7CA22f724B9cF6D13CD920Bf18a3D2'] Stablecoin pools: ['USDC:0x4f3DCf8896ef8287e8AE319BfE0782C4846cD99b', 'USDT:0x179cbE1Acf4e333524b22aAE3BD2AB40869f5b2c', 'USDP:0x09f85CD9B0078957216f8Cd779668dc723DE1771', 'TUSD:0x2238714Ebf832bD2Fb292d47655A7d8E1a9B96ea'] - diff --git a/deployment-logs/mainnet-deleverage-zap.log b/deployment-logs/mainnet-deleverage-zap.log index 6396d7a3..5df19c3d 100644 --- a/deployment-logs/mainnet-deleverage-zap.log +++ b/deployment-logs/mainnet-deleverage-zap.log @@ -1,5 +1,5 @@ -$ ape run scripts/ape-deploy-deleverage-zaps.py deploy --network ethereum:mainnet:alchemy -Enter passphrase to permanently unlock 'babe': +$ ape run scripts/ape-deploy-deleverage-zaps.py deploy --network ethereum:mainnet:alchemy +Enter passphrase to permanently unlock 'babe': WARNING: Danger! This account will now sign any transaction it's given. WARNING: Using cached key for babe INFO: Submitted https://etherscan.io/tx/0x94ac333c7237f052290d666db2cf28dc8de99b4c6281c7c173e3c5d6c1683c7b diff --git a/deployment-logs/mainnet-leverage-zap-router.log b/deployment-logs/mainnet-leverage-zap-router.log index 356a3d83..5772f46c 100644 --- a/deployment-logs/mainnet-leverage-zap-router.log +++ b/deployment-logs/mainnet-leverage-zap-router.log @@ -1,5 +1,5 @@ $ ape run scripts/ape-deploy-leverage-zaps.py deploy --network ethereum:mainnet:alchemy -Enter passphrase to permanently unlock 'babe': +Enter passphrase to permanently unlock 'babe': WARNING: Danger! This account will now sign any transaction it's given. INFO: Compiling 'Controller.vy'. INFO: Compiling 'price_oracles/CryptoWithStablePriceTBTC.vy'. diff --git a/deployment-logs/mainnet-wbtc-eth-mpolicy.log b/deployment-logs/mainnet-wbtc-eth-mpolicy.log index 9297c886..f6fc1e0f 100644 --- a/deployment-logs/mainnet-wbtc-eth-mpolicy.log +++ b/deployment-logs/mainnet-wbtc-eth-mpolicy.log @@ -17,4 +17,3 @@ SUCCESS: Contract 'AggMonetaryPolicy2' deployed to: 0xBB3fda661149f6E45D829D5dd5 Market: ETH Policy: 0xBB3fda661149f6E45D829D5dd54a1608577c5Fa1 Rate: 0.02479623253683494 - diff --git a/deployment-logs/oneway-lending-amm-controller.log b/deployment-logs/oneway-lending-amm-controller.log index a1ee523e..ca0667fb 100644 --- a/deployment-logs/oneway-lending-amm-controller.log +++ b/deployment-logs/oneway-lending-amm-controller.log @@ -9,4 +9,3 @@ Deployed contracts: AMM implementation: 0xDf41E21dAe8Bf6Ae3eddb83337f8364Eb7FC1659 Controller implementation: 0x747459fC40d80a500440F9d650818F7FA5754aCC ========================== - diff --git a/deployment-logs/oneway-lending-redeploy-3.log b/deployment-logs/oneway-lending-redeploy-3.log index 4656a75f..fb74e1ce 100644 --- a/deployment-logs/oneway-lending-redeploy-3.log +++ b/deployment-logs/oneway-lending-redeploy-3.log @@ -33,4 +33,3 @@ tx broadcasted: 0x5146ba1acc78b22d6ab7d7ec8d4e40863e5f327c06817e08c9adb180950c4e tx broadcasted: 0xa825198d27afe3f29d5d3872d0e55ccf8c4b4ace19522eb1297003a8d143816c 0xa825198d27afe3f29d5d3872d0e55ccf8c4b4ace19522eb1297003a8d143816c mined in block 0x02ceb9165e352f3e9cde3f92c907823e8d09413d24beeb2102dc494acf2f3879! Vault CRV-short: 0x4D2f44B0369f3C20c3d670D2C26b048985598450, gauge: 0x99440E11485Fc623c7A9F2064B97A961a440246b - diff --git a/deployment-logs/oneway-lending-redeploy.log b/deployment-logs/oneway-lending-redeploy.log index 7f535ec9..4068ed28 100644 --- a/deployment-logs/oneway-lending-redeploy.log +++ b/deployment-logs/oneway-lending-redeploy.log @@ -28,4 +28,3 @@ tx broadcasted: 0x5515fe7037465df57a55d639486ba28b1d4c9361c8a9e1ae78d32a04e89d29 tx broadcasted: 0x33cdda63b6af94161e9aca1074e326689a8dd6171d5f66728ca17bc58e80a870 0x33cdda63b6af94161e9aca1074e326689a8dd6171d5f66728ca17bc58e80a870 mined in block 0x96df749586526f8ac2f7632e652576b5f337b660ef673d50aa406ee0e584e278! Vault CRV-short: 0xe503Fa25a79D127346815d8d7323B3EfF1679537, gauge: 0xd89279488C9D0237f8F603CcF4e091Aee0246d97 - diff --git a/deployment-logs/optimism-lending.log b/deployment-logs/optimism-lending.log index 193b19a5..a9c8bf18 100644 --- a/deployment-logs/optimism-lending.log +++ b/deployment-logs/optimism-lending.log @@ -1,5 +1,5 @@ -$ python3 scripts/deploy-lending-optimism.py --markets -Password: +$ python3 scripts/deploy-lending-optimism.py --markets +Password: tx broadcasted: 0x85e80b69609910d93e9017261eefbac6d4429fb80943d4528a8f9db9365a2378 /usr/lib/pypy3.10/functools.py:993: UserWarning: debug_traceTransaction not available! titanoboa will try hard to interact with the network, but this means that titanoboa is not able to do certain safety checks at runtime. it is recommended to switch to a node or provider with debug_traceTransaction. val = self.func(instance) @@ -69,4 +69,3 @@ tx broadcasted: 0xced63729517e29dbd8df27d49302f04ccf02a1d5e2483efe37b65fdde12272 Deploying on Ethereum with salt: 0d529a8f69396bfd736d1e560e16cee4a2f41f757be01ef4e2feb4f49e8280ef tx broadcasted: 0xe97906d5af411202d7c6f297713fbd937a33420c35fcde6366a47519214cad26 0xe97906d5af411202d7c6f297713fbd937a33420c35fcde6366a47519214cad26 mined in block 0x5297f360e632f40e3d2b66022c079f9e2590f15f3214cd40dfd40c53f3612c67! - diff --git a/deployment-logs/sepolia-test.log b/deployment-logs/sepolia-test.log index 3e7bf233..2c8c47c3 100644 --- a/deployment-logs/sepolia-test.log +++ b/deployment-logs/sepolia-test.log @@ -2,7 +2,7 @@ $ ape run scripts/ape-deploy-sepolia.py deploy --network ethereum:sepolia:alchem [notice] A new release of pip is available: 23.0.1 -> 23.1.1 [notice] To update, run: pip install --upgrade pip -Enter passphrase to permanently unlock 'babe': +Enter passphrase to permanently unlock 'babe': WARNING: Danger! This account will now sign any transaction it's given. Using cached key for 'babe' INFO: Submitted 0xce926cba43811f2d2ec07937b875cea1126057e31a563ab2e3d0d5a558104d79 diff --git a/deployment-logs/sonic-deployment-2.log b/deployment-logs/sonic-deployment-2.log index 3e3fc8ec..519188d4 100644 --- a/deployment-logs/sonic-deployment-2.log +++ b/deployment-logs/sonic-deployment-2.log @@ -73,4 +73,3 @@ tx broadcasted: 0x2f0a2f812fd0f0d9b968485d63e8d84692e5c390ae640c6e3a91d5000a215b Deploying on Ethereum scUSD with salt: dd47333d339c1a959718371055c6901c3e6ee2c9ea2116e59fd79c85d2591ec3 tx broadcasted: 0x2960e0b70ed752efe6366adf164577776a880112a13cea1be0666d9788382c85 0x2960e0b70ed752efe6366adf164577776a880112a13cea1be0666d9788382c85 mined in block 0x6b757caadbbf68c162401e5a605e513f773d1585dab7ec5b0992c85145c8379f! - diff --git a/deployment-logs/sonic-deployment.log b/deployment-logs/sonic-deployment.log index 6bf18c78..87418fcf 100644 --- a/deployment-logs/sonic-deployment.log +++ b/deployment-logs/sonic-deployment.log @@ -76,4 +76,3 @@ Maybe -32601: the method debug_traceTransaction does not exist/is not available Deploying on Ethereum with salt: 0090c380157938101863e83f3c91ab54b041203dd0e3f0cfcff255f1f9a11885 tx broadcasted: 0xd9b15dd9cd56cb16977024db29643132972eb00a6bfb19f72de294ffb0b7bb28 0xd9b15dd9cd56cb16977024db29643132972eb00a6bfb19f72de294ffb0b7bb28 mined in block 0x9988fafa339cf3daaebdcded817eea0091096f43060699959d9b2222df7ff282! - diff --git a/doc/whitepaper/curve-stablecoin.lyx b/doc/whitepaper/curve-stablecoin.lyx index 00f63db0..378f2386 100644 --- a/doc/whitepaper/curve-stablecoin.lyx +++ b/doc/whitepaper/curve-stablecoin.lyx @@ -220,7 +220,7 @@ name "fig:p_o" \end_inset -Behavior of an +Behavior of an \begin_inset Quotes eld \end_inset @@ -229,50 +229,50 @@ AMM with an external price source \end_inset . - External price + External price \begin_inset Formula $p_{center}$ \end_inset determines a price around which liquidity is formed. - AMM supports liquidity concentrated from prices + AMM supports liquidity concentrated from prices \begin_inset Formula $p_{cd}$ \end_inset - to + to \begin_inset Formula $p_{cu}$ \end_inset -, +, \begin_inset Formula $p_{cd}0,n\right)=p_{cd}\left(n\right)=p_{cu}\left(n-1\right), \end{equation} @@ -932,7 +932,7 @@ p\left(x=0,y>0,n\right)=p_{cd}\left(n\right)=p_{cu}\left(n-1\right), \end_layout \begin_layout Standard -\begin_inset Formula +\begin_inset Formula \begin{equation} p\left(x>0,y=0,n\right)=p_{cu}\left(n\right)=p_{cd}\left(n+1\right), \end{equation} @@ -944,7 +944,7 @@ which shows that there are no gaps between the bands. \begin_layout Standard Trades occur while preserving the invariant from Eq. - + \begin_inset CommandInset ref LatexCommand ref reference "eq:inv" @@ -954,16 +954,16 @@ noprefix "false" \end_inset -, however the current price inside the AMM shifts when the price +, however the current price inside the AMM shifts when the price \begin_inset Formula $p_{o}$ \end_inset -: it goes up when +: it goes up when \begin_inset Formula $p_{o}$ \end_inset goes down and vice versa cubically, as can be seen from Eq. - + \begin_inset CommandInset ref LatexCommand ref reference "eq:current-price" @@ -996,7 +996,7 @@ When a user deposits collateral and borrows a stablecoin, the LLAMMA smart contract calculates the bands where to locate the collateral. When the price of the collateral changes, it starts getting converted to the stablecoin. - When the system is + When the system is \begin_inset Quotes eld \end_inset @@ -1004,13 +1004,13 @@ underwater \begin_inset Quotes erd \end_inset -, user +, user \shape italic already \shape default has enough USD to cover the loan. The amount of stablecoins which can be obtained can be calculated using - a public + a public \series bold get_x_down \series default @@ -1020,16 +1020,16 @@ get_x_down or even weeks after the collateral price went down and sideways, or even will not happen ever if collateral price never goes up or goes back up relatively quickly). - A + A \series bold health \series default - method returns a ratio of + method returns a ratio of \series bold get_x_down \series default to debt plus the value increase in collateral when the price is well above - + \begin_inset Quotes eld \end_inset @@ -1044,15 +1044,15 @@ liquidation When a stablecoin charges interest, this should be reflected in the AMM, too. This is done by adjusting all the grid of prices. - So, when a stablecoin charges interest rate + So, when a stablecoin charges interest rate \begin_inset Formula $r$ \end_inset -, all the grid of prices in the AMM shifts upwards with the same rate +, all the grid of prices in the AMM shifts upwards with the same rate \begin_inset Formula $r$ \end_inset - which is done via a + which is done via a \series bold base_price \series default @@ -1061,35 +1061,35 @@ base_price \end_layout \begin_layout Standard -When we calculate +When we calculate \series bold get_x_down \series default - or + or \series bold get_y_up \series default -, we are first looking for the amounts of stablecoin and collateral +, we are first looking for the amounts of stablecoin and collateral \begin_inset Formula $x_{*}$ \end_inset - and + and \begin_inset Formula $y_{*}$ \end_inset - if current price moves to the current price + if current price moves to the current price \begin_inset Formula $p_{o}$ \end_inset . - Then we look at how much stablecoin or collateral we get if + Then we look at how much stablecoin or collateral we get if \begin_inset Formula $p_{o}$ \end_inset adiabatically changes to either the lowest price of the lowest band, or the highest price of the highest band respectively. This way, we can get a measure of how much stablecoin we will which is - not dependent on the current + not dependent on the current \shape italic instantaneous \shape default @@ -1097,7 +1097,7 @@ instantaneous \end_layout \begin_layout Standard -It is important to point out that the LLAMMA uses +It is important to point out that the LLAMMA uses \begin_inset Formula $p_{o}$ \end_inset @@ -1111,11 +1111,11 @@ It is important to point out that the LLAMMA uses \end_inset ). - If + If \begin_inset Formula $p_{s}<1$ \end_inset -, then price in the LLAMMA is +, then price in the LLAMMA is \begin_inset Formula $p>p_{o}$ \end_inset @@ -1123,7 +1123,7 @@ It is important to point out that the LLAMMA uses \end_layout \begin_layout Standard -In adiabatic approximation, +In adiabatic approximation, \begin_inset Formula $p=p_{o}/p_{s}$ \end_inset @@ -1132,7 +1132,7 @@ In adiabatic approximation, \end_layout \begin_layout Standard -\begin_inset Formula +\begin_inset Formula \begin{equation} p_{o}^{\prime}=p_{o}\sqrt{\frac{p_{o}}{p}}=p_{o}\sqrt{p_{s}}. \end{equation} @@ -1140,11 +1140,11 @@ p_{o}^{\prime}=p_{o}\sqrt{\frac{p_{o}}{p}}=p_{o}\sqrt{p_{s}}. \end_inset At this price, the amount of stablecoins obtained at conversion is higher - by factor of + by factor of \begin_inset Formula $1/p_{s}$ \end_inset - (if + (if \begin_inset Formula $p_{s}<1$ \end_inset @@ -1152,7 +1152,7 @@ At this price, the amount of stablecoins obtained at conversion is higher \end_layout \begin_layout Standard -It is less desirable to have +It is less desirable to have \begin_inset Formula $p_{s}>1$ \end_inset @@ -1164,25 +1164,25 @@ Automatic Stabilizer and Monetary Policy \end_layout \begin_layout Standard -When +When \begin_inset Formula $p_{s}>1$ \end_inset (for example, because of the increased demand for stablecoin), there is peg-keeping reserve formed by an asymmetric deposit into a stableswap Curve pool between the stablecoin and a redeemable reference coin or LP token. - Once + Once \begin_inset Formula $p_{s}>1$ \end_inset , the PegKeeper contract is allowed to mint uncollateralized stablecoin and (only!) deposit it to the stableswap pool single-sided in such a way - that the final price after this is still no less than + that the final price after this is still no less than \begin_inset Formula $1$ \end_inset . - When + When \begin_inset Formula $p_{s}<1$ \end_inset @@ -1190,15 +1190,15 @@ When \end_layout \begin_layout Standard -These actions cause price +These actions cause price \begin_inset Formula $p_{s}$ \end_inset - to quickly depreciate when it's higher than + to quickly depreciate when it's higher than \begin_inset Formula $1$ \end_inset - and appreciate if lower than + and appreciate if lower than \begin_inset Formula $1$ \end_inset @@ -1211,21 +1211,21 @@ These actions cause price \begin_layout Standard Let's denote the amount of stablecoin minted to the stabilizer (debt) as - + \begin_inset Formula $d_{st}$ \end_inset and the function which calculates necessary amount of redeemable USD to - buy the stablecoin in a stableswap AMM + buy the stablecoin in a stableswap AMM \series bold get_dx \series default - as + as \begin_inset Formula $f_{dx}()$ \end_inset . - Then, in order to keep reserves not very large, we use the + Then, in order to keep reserves not very large, we use the \begin_inset Quotes eld \end_inset @@ -1233,7 +1233,7 @@ slow \begin_inset Quotes erd \end_inset - mechanism of stabilization via varying the borrow + mechanism of stabilization via varying the borrow \begin_inset Formula $r$ \end_inset @@ -1241,7 +1241,7 @@ slow \end_layout \begin_layout Standard -\begin_inset Formula +\begin_inset Formula \begin{equation} p_{s}=\frac{f_{dx}(d_{st})}{d_{st}}, \end{equation} @@ -1252,70 +1252,70 @@ p_{s}=\frac{f_{dx}(d_{st})}{d_{st}}, \end_layout \begin_layout Standard -\begin_inset Formula +\begin_inset Formula \begin{equation} r=r_{0}\cdot2^{-\frac{p-1}{h}}, \end{equation} \end_inset -where +where \begin_inset Formula $h$ \end_inset - is the change in + is the change in \begin_inset Formula $p_{s}$ \end_inset - at which the rate + at which the rate \begin_inset Formula $r$ \end_inset - changes by factor of 2 (higher + changes by factor of 2 (higher \begin_inset Formula $p_{s}$ \end_inset - leads to lower + leads to lower \begin_inset Formula $r$ \end_inset ). - The amount of stabilizer debt + The amount of stabilizer debt \begin_inset Formula $d_{st}$ \end_inset - will equilibrate at different value depending on the rate at + will equilibrate at different value depending on the rate at \begin_inset Formula $p_{s}=1$ \end_inset - + \begin_inset Formula $r_{0}$ \end_inset . - Therefore, we can (instead of setting manually) be reducing + Therefore, we can (instead of setting manually) be reducing \begin_inset Formula $r_{0}$ \end_inset - while + while \begin_inset Formula $d_{st}/supply$ \end_inset - is larger than some target number (for example, + is larger than some target number (for example, \begin_inset Formula $5\%$ \end_inset ) (thereby incentivizing borrowers to borrow-and-dump the stablecoin, decreasing - its price and forcing the system to burn the + its price and forcing the system to burn the \begin_inset Formula $d_{st}$ \end_inset ) or increasing if it's lower (thereby incentivizing borrowers to return - loans and pushing + loans and pushing \begin_inset Formula $p_{s}$ \end_inset - up, forcing the system to increase the debt + up, forcing the system to increase the debt \begin_inset Formula $d_{st}$ \end_inset diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index 6966869c..00000000 --- a/docs-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -sphinx diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf1..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 7715482f..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'Curve Stablecoin' -copyright = '2022, CurveFi' -author = 'CurveFi' -release = '0.1.0' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = ['sphinx.ext.extlinks'] - -templates_path = ['_templates'] -exclude_patterns = [] - -add_function_parentheses = False -add_module_names = False - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'alabaster' -html_static_path = ['_static'] - - -extlinks = { - "eip": ("https://eips.ethereum.org/EIPS/eip-%s", "EIP-%s") -} -extlinks_detect_hardcoded_links = True \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 6d0044ab..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. Curve Stablecoin documentation master file, created by - sphinx-quickstart on Wed Nov 9 15:02:30 2022. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Curve Stablecoin's documentation! -============================================ - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - stablecoin - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/source/stablecoin.rst b/docs/source/stablecoin.rst deleted file mode 100644 index b7161888..00000000 --- a/docs/source/stablecoin.rst +++ /dev/null @@ -1,245 +0,0 @@ -Stablecoin Contract -=================== - -.. module:: stablecoin - -Functions ---------- - -.. function:: transferFrom(_from: address, _to: address, _value: uint256) -> bool - - Transfer tokens between two accounts using a previously granted approval from the - originating account to the caller. - - :param address _from: The account tokens will originate from. - :param address _to: The account to credit tokens to. - :param uint256 _value: The amount of tokens to transfer. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the caller has an insufficient allowance granted by the originating account. - :reverts: If the receiving account is either the zero address, or the token contract itself. - :reverts: If the originating account has an insufficient balance. - :log: :class:`Approval` - iff the caller's allowance is less than ``MAX_UINT256``. - :log: :class:`Transfer` - -.. function:: transfer(_to: address, _value: uint256) -> bool - - Transfer tokens from the caller to another account. - - :param address _to: The account to credit tokens to. - :param uint256 _value: The amount of tokens to transfer. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the receiving account is either the zero address, or the token contract itself. - :reverts: If the caller has an insufficient balance. - :log: :class:`Transfer` - -.. function:: approve(_spender: address, _value: uint256) -> bool - - Allow an account to transfer up to ``_value`` amount of the caller's tokens. - - .. note:: - - This function is suceptible to front running as described `here `_. - Use the :func:`increaseAllowance` and :func:`decreaseAllowance` functions to mitigate the described race condition. - - :param address _spender: The account to grant an allowance to. - :param uint256 _value: The total allowance amount. - :returns: ``True`` iff the function is successful. - :rtype: bool - :log: :class:`Approval` - -.. function:: permit(_owner: address, _spender: address, _value: uint256, _deadline: uint256, _v: uint8, _r: bytes32, _s: bytes32) -> bool - - Allow an account to transfer up to ``_value`` amount of ``_owner``'s tokens via a signature. - - :param address _owner: The account granting an approval and which generated the signature provided. - :param address _spender: The account the allowance is granted to. - :param uint256 _value: The total allowance amount. - :param uint256 _deadline: The timestamp after which the signature is considered invalid. - :param uint8 _v: The last byte of the generated signature. - :param bytes32 _r: The first 32 byte chunk of the generated signature. - :param bytes32 _s: The second 32 byte chunk of the generated signature. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the ``_owner`` argument is the zero address. - :reverts: If the ``block.timestamp`` at execution is greater than the ``deadline`` argument. - :reverts: If the recovered signer is not equivalent to the ``_owner`` argument. - :log: :class:`Approval` - -.. function:: increaseAllowance(_spender: address, _add_value: uint256) -> bool - - Increase the allowance granted to ``_spender`` by the caller. - - **If an overflow were to occur, the allowance is instead set to** ``MAX_UINT256``. - - :param address _spender: The account to increase the allowance of. - :param uint256 _add_value: The amount to increase the allowance by. - :returns: ``True`` iff the function is successful. - :rtype: bool - :log: :class:`Approval` - iff the allowance is updated to a new value. - -.. function:: decreaseAllowance(_spender: address, _sub_value: uint256) -> bool - - Decrease the allowance granted to ``_spender`` by the caller. - - **If an underflow were to occur, the allowance is instead set to** ``0``. - - :param address _spender: The account to decrease the allowance of. - :param uint256 _sub_value: The amount to decrease the allowance by. - :returns: ``True`` iff the function is successful. - :rtype: bool - :log: :class:`Approval` - iff the allowance is updated to a new value. - -.. function:: burnFrom(_from: address, _value: uint256) -> bool - - Burn tokens from an account using a previously granted allowance. - - :param address _from: The account to burn tokens from. - :param uint256 _value: The amount of tokens to burn. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the caller has an insufficient allowance. - :reverts: If the account tokens are to be burned from has an insufficient balance. - :log: :class:`Approval` - iff the caller's allowance is less than ``MAX_UINT256``. - :log: :class:`Transfer` - -.. function:: burn(_value: uint256) -> bool - - Burn tokens. - - :param uint256 _value: The amount of tokens to burn. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the caller has an insufficient balance. - :log: :class:`Transfer` - -.. function:: mint(_to: address, _value: uint256) -> bool - - Mint new tokens to ``_to``. - - :param address _to: The account to received the newly minted tokens. - :param uint256 _value: The amount of tokens to mint. - :returns: ``True`` iff the function is successful. - :rtype: bool - :reverts: If the caller is not the :func:`minter`. - :reverts: If the receiving account is either the zero address, or the token contract itself. - :reverts: If the :func:`balanceOf` the receiver overflows. - :reverts: If :func:`totalSupply` overflows. - :log: :class:`Transfer` - -.. function:: add_minter(_minter: address) - - Grant an account the ability to mint tokens. - - :param address _minter: The account to grant permissions to. - :reverts: If the caller is not the :func:`admin` - -.. function:: remove_minter(_minter: address) - - Revoke an account's ability to mint tokens. - - :param address _minter: The account to revoke the permissions of. - :reverts: If the caller is not the :func:`admin` - -.. function:: set_admin(_new_admin: address) - - Set the admin, which is capable of calling :func:`mint`, :func:`add_minter` and :func:`remove_minter`. - - :param address _new_admin: The account to set as the new admin. - :reverts: If the caller is not the current :func:`admin`. - :log: :class:`SetAdmin` - -Read-Only Functions -------------------- - -.. function:: DOMAIN_SEPARATOR() -> bytes32 - - Get the :eip:`712` domain separator for this contract. - - **In the event of a chain fork, this value is updated to prevent replay attacks.** - - :rtype: bytes32 - -.. function:: is_minter(_account: address) -> bool - - Query whether an account is allowed to call the :func:`mint` function. - - :param address _account: The account to query the privilege of. - :returns: Whether an account is allowed to call :func:`mint`. - :rtype: bool - -.. function:: name() -> String[64] - - Get the token contract's full name. - - :rtype: String[64] - -.. function:: symbol() -> String[32] - - Get the token contract's currency symbol. - - :rtype: String[32] - -.. function:: salt() -> bytes32 - - Get the salt value used for calculating the :func:`DOMAIN_SEPARATOR`. - - :rtype: bytes32 - -.. function:: allowance(_owner: address, _spender: address) -> uint256 - - Get the allowance granted to ``_spender`` from ``_owner``. - - :param address _owner: The account tokens will originate from. - :param address _spender: The account allowed to spend ``_owner``'s tokens. - :rtype: uint256 - -.. function:: balanceOf(_owner: address) -> uint256 - - Get the token balance of an account. - - :param address _owner: The account to query the balance of. - :rtype: uint256 - -.. function:: totalSupply() -> uint256 - - Get the total tokens in circulation. - - :rtype: uint256 - -.. function:: nonces(_owner: address) -> uint256 - - Get the :eip:`2612` permit signature nonce of an account. - - :param address _owner: The account to query the nonce of. - :rtype: uint256 - -.. function:: admin() -> address - - Get the account with administrator privileges. - - :rtype: address - -Events ------- - -.. class:: Approval(owner: address, spender: address, value: uint256) - - See :eip:`20`. - -.. class:: Transfer(sender: address, receiver: address, value: uint256) - - See :eip:`20`. - -.. class:: SetAdmin(admin: address) - - Logged when the contract's :func:`admin` changes. - -.. class:: AddMinter(minter: address) - - Logged when an account is granted the minter role. - -.. class:: RemoveMinter(minter: address) - - Logged when an account has it's minter role revoked. diff --git a/model/avg.py b/model/avg.py index 59d74ff5..1dffe8e8 100644 --- a/model/avg.py +++ b/model/avg.py @@ -6,13 +6,13 @@ def calc_price(D, p, sigma=0.001): D = np.array(D) p = np.array(p) p_pre = (D * p).sum() / D.sum() - e = (p - p_pre)**2 / sigma**2 + e = (p - p_pre) ** 2 / sigma**2 e -= e.min() w = D * np.exp(-e) return (w * p).sum() / w.sum() -if __name__ == '__main__': +if __name__ == "__main__": N = 4 D = [100] * N p = np.linspace(0.9, 1.1, 1000) diff --git a/model/rate-secondary-susde.py b/model/rate-secondary-susde.py index 9e3aae9f..0a8418d1 100644 --- a/model/rate-secondary-susde.py +++ b/model/rate-secondary-susde.py @@ -24,16 +24,16 @@ def plot_rate(alpha, beta, u0, r0, *args, **kw): pylab.plot(u, r, *args, **kw) -if __name__ == '__main__': +if __name__ == "__main__": beta = 2.5 r0 = 10 - plot_rate(0.35, 1.5, 0.85, 10, '--', c="gray") + plot_rate(0.35, 1.5, 0.85, 10, "--", c="gray") plot_rate(0.35, 2.5, 0.8, 10, c="black") pylab.grid() - pylab.xlabel('Utilization') - pylab.ylabel('r (%)') + pylab.xlabel("Utilization") + pylab.ylabel("r (%)") pylab.xlim(-0.05, 1.05) pylab.ylim(-beta * r0 * 0.05, beta * r0 * 1.05) pylab.show() diff --git a/model/rate-secondary.py b/model/rate-secondary.py index 24a8dc39..c4872b70 100644 --- a/model/rate-secondary.py +++ b/model/rate-secondary.py @@ -22,8 +22,8 @@ pylab.plot(u, r) pylab.grid() -pylab.xlabel('Utilization') -pylab.ylabel('r (%)') +pylab.xlabel("Utilization") +pylab.ylabel("r (%)") pylab.xlim(-0.05, 1.05) pylab.ylim(-beta * r0 * 0.05, beta * r0 * 1.05) pylab.show() diff --git a/model/rate-usde.py b/model/rate-usde.py index 8bc81b3b..b113f775 100644 --- a/model/rate-usde.py +++ b/model/rate-usde.py @@ -4,17 +4,17 @@ def plot_rate(min_rate, max_rate, *args, **kw): x = np.linspace(0, 1, 100) - r = (max_rate / min_rate)**x * min_rate + r = (max_rate / min_rate) ** x * min_rate pylab.plot(x, r, *args, **kw) -if __name__ == '__main__': - plot_rate(0.5, 25, '--', c="gray") +if __name__ == "__main__": + plot_rate(0.5, 25, "--", c="gray") plot_rate(0.5, 40, c="black") pylab.grid() - pylab.xlabel('Utilization') - pylab.ylabel('r (%)') + pylab.xlabel("Utilization") + pylab.ylabel("r (%)") pylab.xlim(-0.05, 1.05) pylab.ylim(0, 45) pylab.show() diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 016c89cd..00000000 --- a/poetry.lock +++ /dev/null @@ -1,3363 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "bitarray" -version = "2.9.2" -description = "efficient arrays of booleans -- C extension" -optional = false -python-versions = "*" -files = [ - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:917905de565d9576eb20f53c797c15ba88b9f4f19728acabec8d01eee1d3756a"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b35bfcb08b7693ab4bf9059111a6e9f14e07d57ac93cd967c420db58ab9b71e1"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea1923d2e7880f9e1959e035da661767b5a2e16a45dfd57d6aa831e8b65ee1bf"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0b63a565e8a311cc8348ff1262d5784df0f79d64031d546411afd5dd7ef67d"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf0620da2b81946d28c0b16f3e3704d38e9837d85ee4f0652816e2609aaa4fed"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79a9b8b05f2876c7195a2b698c47528e86a73c61ea203394ff8e7a4434bda5c8"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:345c76b349ff145549652436235c5532e5bfe9db690db6f0a6ad301c62b9ef21"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e2936f090bf3f4d1771f44f9077ebccdbc0415d2b598d51a969afcb519df505"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f9346e98fc2abcef90b942973087e2462af6d3e3710e82938078d3493f7fef52"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6ec283d4741befb86e8c3ea2e9ac1d17416c956d392107e45263e736954b1f7"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:962892646599529917ef26266091e4cb3077c88b93c3833a909d68dcc971c4e3"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e8da5355d7d75a52df5b84750989e34e39919ec7e59fafc4c104cc1607ab2d31"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:603e7d640e54ad764d2b4da6b61e126259af84f253a20f512dd10689566e5478"}, - {file = "bitarray-2.9.2-cp310-cp310-win32.whl", hash = "sha256:f00079f8e69d75c2a417de7961a77612bb77ef46c09bc74607d86de4740771ef"}, - {file = "bitarray-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:1bb33673e7f7190a65f0a940c1ef63266abdb391f4a3e544a47542d40a81f536"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fe71fd4b76380c2772f96f1e53a524da7063645d647a4fcd3b651bdd80ca0f2e"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d527172919cdea1e13994a66d9708a80c3d33dedcf2f0548e4925e600fef3a3a"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:052c5073bdcaa9dd10628d99d37a2f33ec09364b86dd1f6281e2d9f8d3db3060"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e064caa55a6ed493aca1eda06f8b3f689778bc780a75e6ad7724642ba5dc62f7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:508069a04f658210fdeee85a7a0ca84db4bcc110cbb1d21f692caa13210f24a7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4da73ebd537d75fa7bccfc2228fcaedea0803f21dd9d0bf0d3b67fef3c4af294"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb378eaa65cd43098f11ff5d27e48ee3b956d2c00d2d6b5bfc2a09fe183be47"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d14c790b91f6cbcd9b718f88ed737c78939980c69ac8c7f03dd7e60040c12951"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eea9318293bc0ea6447e9ebfba600a62f3428bea7e9c6d42170ae4f481dbab3"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b76ffec27c7450b8a334f967366a9ebadaea66ee43f5b530c12861b1a991f503"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:76b76a07d4ee611405045c6950a1e24c4362b6b44808d4ad6eea75e0dbc59af4"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c7d16beeaaab15b075990cd26963d6b5b22e8c5becd131781514a00b8bdd04bd"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60df43e868a615c7e15117a1e1c2e5e11f48f6457280eba6ddf8fbefbec7da99"}, - {file = "bitarray-2.9.2-cp311-cp311-win32.whl", hash = "sha256:e788608ed7767b7b3bbde6d49058bccdf94df0de9ca75d13aa99020cc7e68095"}, - {file = "bitarray-2.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:a23397da092ef0a8cfe729571da64c2fc30ac18243caa82ac7c4f965087506ff"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:90e3a281ffe3897991091b7c46fca38c2675bfd4399ffe79dfeded6c52715436"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bed637b674db5e6c8a97a4a321e3e4d73e72d50b5c6b29950008a93069cc64cd"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e49066d251dbbe4e6e3a5c3937d85b589e40e2669ad0eef41a00f82ec17d844b"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4344e96642e2211fb3a50558feff682c31563a4c64529a931769d40832ca79"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aeb60962ec4813c539a59fbd4f383509c7222b62c3fb1faa76b54943a613e33a"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f7982f10581bb16553719e5e8f933e003f5b22f7d25a68bdb30fac630a6ff"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71d1cabdeee0cdda4669168618f0e46b7dace207b29da7b63aaa1adc2b54081"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0ef2d0a6f1502d38d911d25609b44c6cc27bee0a4363dd295df78b075041b60"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6f71d92f533770fb027388b35b6e11988ab89242b883f48a6fe7202d238c61f8"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba0734aa300757c924f3faf8148e1b8c247176a0ac8e16aefdf9c1eb19e868f7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:d91406f413ccbf4af6ab5ae7bc78f772a95609f9ddd14123db36ef8c37116d95"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:87abb7f80c0a042f3fe8e5264da1a2756267450bb602110d5327b8eaff7682e7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b558ce85579b51a2e38703877d1e93b7728a7af664dd45a34e833534f0b755d"}, - {file = "bitarray-2.9.2-cp312-cp312-win32.whl", hash = "sha256:dac2399ee2889fbdd3472bfc2ede74c34cceb1ccf29a339964281a16eb1d3188"}, - {file = "bitarray-2.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:48a30d718d1a6dfc22a49547450107abe8f4afdf2abdcbe76eb9ed88edc49498"}, - {file = "bitarray-2.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2c6be1b651fad8f3adb7a5aa12c65b612cd9b89530969af941844ae680f7d981"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5b399ae6ab975257ec359f03b48fc00b1c1cd109471e41903548469b8feae5c"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b3543c8a1cb286ad105f11c25d8d0f712f41c5c55f90be39f0e5a1376c7d0b0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03adaacb79e2fb8f483ab3a67665eec53bb3fd0cd5dbd7358741aef124688db3"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae5b0657380d2581e13e46864d147a52c1e2bbac9f59b59c576e42fa7d10cf0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1f4bf6ea8eb9d7f30808c2e9894237a96650adfecbf5f3643862dc5982f89e"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a8873089be2aa15494c0f81af1209f6e1237d762c5065bc4766c1b84321e1b50"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:677e67f50e2559efc677a4366707070933ad5418b8347a603a49a070890b19bc"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:a620d8ce4ea2f1c73c6b6b1399e14cb68c6915e2be3fad5808c2998ed55b4acf"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:64115ccabbdbe279c24c367b629c6b1d3da9ed36c7420129e27c338a3971bfee"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d6fb422772e75385b76ad1c52f45a68bd4efafd8be8d0061c11877be74c4d43"}, - {file = "bitarray-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:852e202875dd6dfd6139ce7ec4e98dac2b17d8d25934dc99900831e81c3adaef"}, - {file = "bitarray-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7dfefdcb0dc6a3ba9936063cec65a74595571b375beabe18742b3d91d087eefd"}, - {file = "bitarray-2.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b306c4cf66912511422060f7f5e1149c8bdb404f8e00e600561b0749fdd45659"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a09c4f81635408e3387348f415521d4b94198c562c23330f560596a6aaa26eaf"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5361413fd2ecfdf44dc8f065177dc6aba97fa80a91b815586cb388763acf7f8d"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8a9475d415ef1eaae7942df6f780fa4dcd48fce32825eda591a17abba869299"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b87baa7bfff9a5878fcc1bffe49ecde6e647a72a64b39a69cd8a2992a43a34"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6b86cfdfc503e92cb71c68766a24565359136961642504a7cc9faf936d9c88"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd56b8ae87ebc71bcacbd73615098e8a8de952ecbb5785b6b4e2b07da8a06e1f"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3fa909cfd675004aed8b4cc9df352415933656e0155a6209d878b7cb615c787e"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b069ca9bf728e0c5c5b60e00a89df9af34cc170c695c3bfa3b372d8f40288efb"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6067f2f07a7121749858c7daa93c8774325c91590b3e81a299621e347740c2ae"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:321841cdad1dd0f58fe62e80e9c9c7531f8ebf8be93f047401e930dc47425b1e"}, - {file = "bitarray-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:54e16e32e60973bb83c315de9975bc1bcfc9bd50bb13001c31da159bc49b0ca1"}, - {file = "bitarray-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4dcadb7b8034aa3491ee8f5a69b3d9ba9d7d1e55c3cc1fc45be313e708277f8"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c8919fdbd3bb596b104388b56ae4b266eb28da1f2f7dff2e1f9334a21840fe96"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb7a9d8a2e400a1026de341ad48e21670a6261a75b06df162c5c39b0d0e7c8f4"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ec84668dd7b937874a2b2c293cd14ba84f37be0d196dead852e0ada9815d807"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2de9a31c34e543ae089fd2a5ced01292f725190e379921384f695e2d7184bd3"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9521f49ae121a17c0a41e5112249e6fa7f6a571245b1118de81fb86e7c1bc1ce"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6cc6545d6d76542aee3d18c1c9485fb7b9812b8df4ebe52c4535ec42081b48f"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bbe1616425f71c0df5ef2e8755e878d9504d5a531acba58ab4273c52c117a"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4bba8042ea6ab331ade91bc435d81ad72fddb098e49108610b0ce7780c14e68"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a035da89c959d98afc813e3c62f052690d67cfd55a36592f25d734b70de7d4b0"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d70b1579da7fb71be5a841a1f965d19aca0ef27f629cfc07d06b09aafd0a333"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:405b83bed28efaae6d86b6ab287c75712ead0adbfab2a1075a1b7ab47dad4d62"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7eb8be687c50da0b397d5e0ab7ca200b5ebb639e79a9f5e285851d1944c94be9"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eceb551dfeaf19c609003a69a0cf8264b0efd7abc3791a11dfabf4788daf0d19"}, - {file = "bitarray-2.9.2-cp38-cp38-win32.whl", hash = "sha256:bb198c6ed1edbcdaf3d1fa3c9c9d1cdb7e179a5134ef5ee660b53cdec43b34e7"}, - {file = "bitarray-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:648d2f2685590b0103c67a937c2fb9e09bcc8dfb166f0c7c77bd341902a6f5b3"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ea816dc8f8e65841a8bbdd30e921edffeeb6f76efe6a1eb0da147b60d539d1cf"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d0e32530f941c41eddfc77600ec89b65184cb909c549336463a738fab3ed285"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a22266fb416a3b6c258bf7f83c9fe531ba0b755a56986a81ad69dc0f3bcc070"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6d3e80dd8239850f2604833ff3168b28909c8a9357abfed95632cccd17e3e7"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f135e804986b12bf14f2cd1eb86674c47dea86c4c5f0fa13c88978876b97ebe6"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87580c7f7d14f7ec401eda7adac1e2a25e95153e9c339872c8ae61b3208819a1"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64b433e26993127732ac7b66a7821b2537c3044355798de7c5fcb0af34b8296f"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e497c535f2a9b68c69d36631bf2dba243e05eb343b00b9c7bbdc8c601c6802d"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e40b3cb9fa1edb4e0175d7c06345c49c7925fe93e39ef55ecb0bc40c906b0c09"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f2f8692f95c9e377eb19ca519d30d1f884b02feb7e115f798de47570a359e43f"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f0b84fc50b6dbeced4fa390688c07c10a73222810fb0e08392bd1a1b8259de36"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d656ad38c942e38a470ddbce26b5020e08e1a7ea86b8fd413bb9024b5189993a"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ab0f1dbfe5070db98771a56aa14797595acd45a1af9eadfb193851a270e7996"}, - {file = "bitarray-2.9.2-cp39-cp39-win32.whl", hash = "sha256:0a99b23ac845a9ea3157782c97465e6ae026fe0c7c4c1ed1d88f759fd6ea52d9"}, - {file = "bitarray-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:9bbcfc7c279e8d74b076e514e669b683f77b4a2a328585b3f16d4c5259c91222"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:43847799461d8ba71deb4d97b47250c2c2fb66d82cd3cb8b4caf52bb97c03034"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f44381b0a4bdf64416082f4f0e7140377ae962c0ced6f983c6d7bbfc034040"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a484061616fb4b158b80789bd3cb511f399d2116525a8b29b6334c68abc2310f"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ff9e38356cc803e06134cf8ae9758e836ccd1b793135ef3db53c7c5d71e93bc"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b44105792fbdcfbda3e26ee88786790fda409da4c71f6c2b73888108cf8f062f"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7e913098de169c7fc890638ce5e171387363eb812579e637c44261460ac00aa2"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6fe315355cdfe3ed22ef355b8bdc81a805ca4d0949d921576560e5b227a1112"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f708e91fdbe443f3bec2df394ed42328fb9b0446dff5cb4199023ac6499e09fd"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7b09489b71f9f1f64c0fa0977e250ec24500767dab7383ba9912495849cadf"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:128cc3488176145b9b137fdcf54c1c201809bbb8dd30b260ee40afe915843b43"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:21f21e7f56206be346bdbda2a6bdb2165a5e6a11821f88fd4911c5a6bbbdc7e2"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f4dd3af86dd8a617eb6464622fb64ca86e61ce99b59b5c35d8cd33f9c30603d"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6465de861aff7a2559f226b37982007417eab8c3557543879987f58b453519bd"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbaf2bb71d6027152d603f1d5f31e0dfd5e50173d06f877bec484e5396d4594b"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f32948c86e0d230a296686db28191b67ed229756f84728847daa0c7ab7406e3"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be94e5a685e60f9d24532af8fe5c268002e9016fa80272a94727f435de3d1003"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5cc9381fd54f3c23ae1039f977bfd6d041a5c3c1518104f616643c3a5a73b15"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd926e8ae4d1ed1ac4a8f37212a62886292f692bc1739fde98013bf210c2d175"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:461a3dafb9d5fda0bb3385dc507d78b1984b49da3fe4c6d56c869a54373b7008"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:393cb27fd859af5fd9c16eb26b1c59b17b390ff66b3ae5d0dd258270191baf13"}, - {file = "bitarray-2.9.2.tar.gz", hash = "sha256:a8f286a51a32323715d77755ed959f94bef13972e9a2fe71b609e40e6d27957e"}, -] - -[[package]] -name = "build" -version = "1.2.1" -description = "A simple, correct Python build frontend" -optional = false -python-versions = ">=3.8" -files = [ - {file = "build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4"}, - {file = "build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} -packaging = ">=19.1" -pyproject_hooks = "*" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] -test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] -typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] -uv = ["uv (>=0.1.18)"] -virtualenv = ["virtualenv (>=20.0.35)"] - -[[package]] -name = "cachecontrol" -version = "0.14.0" -description = "httplib2 caching for requests" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, - {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, -] - -[package.dependencies] -filelock = {version = ">=3.8.0", optional = true, markers = "extra == \"filecache\""} -msgpack = ">=0.5.2,<2.0.0" -requests = ">=2.16.0" - -[package.extras] -dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "furo", "mypy", "pytest", "pytest-cov", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] -filecache = ["filelock (>=3.8.0)"] -redis = ["redis (>=2.10.5)"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -optional = false -python-versions = "*" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] - -[[package]] -name = "cbor2" -version = "5.6.4" -description = "CBOR (de)serializer with extensive tag support" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cbor2-5.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c40c68779a363f47a11ded7b189ba16767391d5eae27fac289e7f62b730ae1fc"}, - {file = "cbor2-5.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0625c8d3c487e509458459de99bf052f62eb5d773cc9fc141c6a6ea9367726d"}, - {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7137622204168c3a57882f15dd09b5135bda2bcb1cf8b56b58d26b5150dfca"}, - {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3545e1e62ec48944b81da2c0e0a736ca98b9e4653c2365cae2f10ae871e9113"}, - {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6749913cd00a24eba17406a0bfc872044036c30a37eb2fcde7acfd975317e8a"}, - {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57db966ab08443ee54b6f154f72021a41bfecd4ba897fe108728183ad8784a2a"}, - {file = "cbor2-5.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:380e0c7f4db574dcd86e6eee1b0041863b0aae7efd449d49b0b784cf9a481b9b"}, - {file = "cbor2-5.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c763d50a1714e0356b90ad39194fc8ef319356b89fb001667a2e836bfde88e3"}, - {file = "cbor2-5.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58a7ac8861857a9f9b0de320a4808a2a5f68a2599b4c14863e2748d5a4686c99"}, - {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d715b2f101730335e84a25fe0893e2b6adf049d6d44da123bf243b8c875ffd8"}, - {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f53a67600038cb9668720b309fdfafa8c16d1a02570b96d2144d58d66774318"}, - {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f898bab20c4f42dca3688c673ff97c2f719b1811090430173c94452603fbcf13"}, - {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e5d50fb9f47d295c1b7f55592111350424283aff4cc88766c656aad0300f11f"}, - {file = "cbor2-5.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7f9d867dcd814ab8383ad132eb4063e2b69f6a9f688797b7a8ca34a4eadb3944"}, - {file = "cbor2-5.6.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e0860ca88edf8aaec5461ce0e498eb5318f1bcc70d93f90091b7a1f1d351a167"}, - {file = "cbor2-5.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c38a0ed495a63a8bef6400158746a9cb03c36f89aeed699be7ffebf82720bf86"}, - {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8d8c2f208c223a61bed48dfd0661694b891e423094ed30bac2ed75032142aa"}, - {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cd2ce6136e1985da989e5ba572521023a320dcefad5d1fff57fba261de80ca"}, - {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7facce04aed2bf69ef43bdffb725446fe243594c2451921e89cc305bede16f02"}, - {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f9c8ee0d89411e5e039a4f3419befe8b43c0dd8746eedc979e73f4c06fe0ef97"}, - {file = "cbor2-5.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:9b45d554daa540e2f29f1747df9f08f8d98ade65a67b1911791bc193d33a5923"}, - {file = "cbor2-5.6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a5cb2c16687ccd76b38cfbfdb34468ab7d5635fb92c9dc5e07831c1816bd0a9"}, - {file = "cbor2-5.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f985f531f7495527153c4f66c8c143e4cf8a658ec9e87b14bc5438e0a8d0911"}, - {file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d9c7b4bd7c3ea7e5587d4f1bbe073b81719530ddadb999b184074f064896e2"}, - {file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d06184dcdc275c389fee3cd0ea80b5e1769763df15f93ecd0bf4c281817365"}, - {file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e9ba7116f201860fb4c3e80ef36be63851ec7e4a18af70fea22d09cab0b000bf"}, - {file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:341468ae58bdedaa05c907ab16e90dd0d5c54d7d1e66698dfacdbc16a31e815b"}, - {file = "cbor2-5.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:bcb4994be1afcc81f9167c220645d878b608cae92e19f6706e770f9bc7bbff6c"}, - {file = "cbor2-5.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41c43abffe217dce70ae51c7086530687670a0995dfc90cc35f32f2cf4d86392"}, - {file = "cbor2-5.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:227a7e68ba378fe53741ed892b5b03fe472b5bd23ef26230a71964accebf50a2"}, - {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13521b7c9a0551fcc812d36afd03fc554fa4e1b193659bb5d4d521889aa81154"}, - {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4816d290535d20c7b7e2663b76da5b0deb4237b90275c202c26343d8852b8a"}, - {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1e98d370106821335efcc8fbe4136ea26b4747bf29ca0e66512b6c4f6f5cc59f"}, - {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:68743a18e16167ff37654a29321f64f0441801dba68359c82dc48173cc6c87e1"}, - {file = "cbor2-5.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:7ba5e9c6ed17526d266a1116c045c0941f710860c5f2495758df2e0d848c1b6d"}, - {file = "cbor2-5.6.4-py3-none-any.whl", hash = "sha256:fe411c4bf464f5976605103ebcd0f60b893ac3e4c7c8d8bc8f4a0cb456e33c60"}, - {file = "cbor2-5.6.4.tar.gz", hash = "sha256:1c533c50dde86bef1c6950602054a0ffa3c376e8b0e20c7b8f5b108793f6983e"}, -] - -[package.extras] -benchmarks = ["pytest-benchmark (==4.0.0)"] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] -test = ["coverage (>=7)", "hypothesis", "pytest"] - -[[package]] -name = "certifi" -version = "2024.6.2" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "ckzg" -version = "1.0.2" -description = "Python bindings for C-KZG-4844" -optional = false -python-versions = "*" -files = [ - {file = "ckzg-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdd082bc0f2a595e3546658ecbe1ff78fe65b0ab7e619a8197a62d94f46b5b46"}, - {file = "ckzg-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50ca4af4e2f1a1e8b0a7e97b3aef39dedbb0d52d90866ece424f13f8df1b5972"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e9dc671b0a307ea65d0a216ca496c272dd3c1ed890ddc2a306da49b0d8ffc83"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d95e97a0d0f7758119bb905fb5688222b1556de465035614883c42fe4a047d1f"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27261672154cbd477d84d289845b0022fbdbe2ba45b7a2a2051c345fa04c8334"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c16d5ee1ddbbbad0367ff970b3ec9f6d1879e9f928023beda59ae9e16ad99e4c"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:09043738b029bdf4fdc82041b395cfc6f5b5cf63435e5d4d685d24fd14c834d3"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3c0afa232d2312e3101aaddb6971b486b0038a0f9171500bc23143f5749eff55"}, - {file = "ckzg-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:96e8281b6d58cf91b9559e1bd38132161d63467500838753364c68e825df2e2c"}, - {file = "ckzg-1.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b874167de1d6de72890a2ad5bd9aa7adbddc41c3409923b59cf4ef27f83f79da"}, - {file = "ckzg-1.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d2ccd68b0743e20e853e31a08da490a8d38c7f12b9a0c4ee63ef5afa0dc2427"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e8d534ddbe785c44cf1cd62ee32d78b4310d66dd70e42851f5468af655b81f5"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c732cda00c76b326f39ae97edfc6773dd231b7c77288b38282584a7aee77c3a7"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abc5a27284db479ead4c053ff086d6e222914f1b0aa08b80eabfa116dbed4f7a"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6bd5006cb3e802744309450183087a6594d50554814eee19065f7064dff7b05"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3594470134eda7adf2813ad3f1da55ced98c8a393262f47ce3890c5afa05b23e"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fea56f39e48b60c1ff6f751c47489e353d1bd95cae65c429cf5f87735d794431"}, - {file = "ckzg-1.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:f769eb2e1056ca396462460079f6849c778f58884bb24b638ff7028dd2120b65"}, - {file = "ckzg-1.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e3cb2f8c767aee57e88944f90848e8689ce43993b9ff21589cfb97a562208fe7"}, - {file = "ckzg-1.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b29889f5bc5db530f766871c0ff4133e7270ecf63aaa3ca756d3b2731980802"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfcc70fb76b3d36125d646110d5001f2aa89c1c09ff5537a4550cdb7951f44d4"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ca8a256cdd56d06bc5ef24caac64845240dbabca402c5a1966d519b2514b4ec"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ea91b0236384f93ad1df01d530672f09e254bd8c3cf097ebf486aebb97f6c8c"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65311e72780105f239d1d66512629a9f468b7c9f2609b8567fc68963ac638ef9"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d7600ce7a73ac41d348712d0c1fe5e4cb6caa329377064cfa3a6fd8fbffb410"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19893ee7bd7da8688382cb134cb9ee7bce5c38e3a9386e3ed99bb010487d2d17"}, - {file = "ckzg-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3e1a9a72695e777497e95bb2213316a1138f82d1bb5d67b9c029a522d24908e"}, - {file = "ckzg-1.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2f59da9cb82b6a4be615f2561a255731eededa7ecd6ba4b2f2dedfc918ef137"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c915e1f2ef51657c3255d8b1e2aea6e0b93348ae316b2b79eaadfb17ad8f514e"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcc0d2031fcabc4be37e9e602c926ef9347238d2f58c1b07e0c147f60b9e760b"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cdaad2745425d7708e76e8e56a52fdaf5c5cc1cfefd5129d24ff8dbe06a012d"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1ec775649daade1b93041aac9c1660c2ad9828b57ccd2eeb5a3074d8f05e544a"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:02f9cc3e38b3702ec5895a1ebf927fd02b8f5c2f93c7cb9e438581b5b74472c8"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0e816af31951b5e94e6bc069f21fe783427c190526e0437e16c4488a34ddcacc"}, - {file = "ckzg-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:651ba33ee2d7fefff14ca519a72996b733402f8b043fbfef12d5fe2a442d86d8"}, - {file = "ckzg-1.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:489763ad92e2175fb6ab455411f03ec104c630470d483e11578bf2e00608f283"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69e1376284e9a5094d7c4d3e552202d6b32a67c5acc461b0b35718d8ec5c7363"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9d0b09ca1bdb5955b626d6645f811424ae0fcab47699a1a938a3ce0438c25f"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87a121ace8feb6c9386f247e7e36ef55e584fc8a6b1bc2c60757a59c1efe364"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:97c27153fab853f017fed159333b27beeb2e0da834c92c9ecdc26d0e5c3983b3"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b26799907257c39471cb3665f66f7630797140131606085c2c94a7094ab6ddf2"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:283a40c625222560fda3dcb912b666f7d50f9502587b73c4358979f519f1c961"}, - {file = "ckzg-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5f029822d27c52b9c3dbe5706408b099da779f10929be0422a09a34aa026a872"}, - {file = "ckzg-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edaea8fb50b01c6c19768d9305ad365639a8cd804754277d5108dcae4808f00b"}, - {file = "ckzg-1.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27be65c88d5d773a30e6f198719cefede7e25cad807384c3d65a09c11616fc9d"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9ac729c5c6f3d2c030c0bc8c9e10edc253e36f002cfe227292035009965d349"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1528bc2b95aac6d184a90b023602c40d7b11b577235848c1b5593c00cf51d37"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071dc7fc179316ce1bfabaa056156e4e84f312c4560ab7b9529a3b9a84019df3"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:895044069de7010be6c7ee703f03fd7548267a0823cf60b9dd26ec50267dd9e8"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ed8c99cd3d9af596470e0481fd58931007288951719bad026f0dd486dd0ec11"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74d87eafe561d4bfb544a4f3419d26c56ad7de00f39789ef0fdb09515544d12e"}, - {file = "ckzg-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:54d71e5ca416bd51c543f9f51e426e6792f8a0280b83aef92faad1b826f401ea"}, - {file = "ckzg-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da2d9988781a09a4577ee7ea8f51fe4a94b4422789a523164f5ba3118566ad41"}, - {file = "ckzg-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9e030af7d6acdcb356fddfb095048bc8e880fe4cd70ff2206c64f33bf384a0d"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:145ae31c3d499d1950567bd636dc5b24292b600296b9deb5523bc20d8f7b51c3"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d81e68e84d80084da298471ad5eaddfcc1cf73545cb24e9453550c8186870982"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67064bbbeba1a6892c9c80b3d0c2a540ff48a5ca5356fdb2a8d998b264e43e6"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99694917eb6decefc0d330d9887a89ea770824b2fa76eb830bab5fe57ea5c20c"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fca227ce0ce3427254a113fdb3aed5ecd99c1fc670cb0c60cc8a2154793678e4"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a66a690d3d1801085d11de6825df47a99b465ff32dbe90be4a3c9f43c577da96"}, - {file = "ckzg-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:272adfe471380d10e4a0e1639d877e504555079a60233dd82249c799b15be81e"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f37be0054ebb4b8ac6e6d5267290b239b09e7ddc611776051b4c3c4032d161ba"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:611c03a170f0f746180eeb0cc28cdc6f954561b8eb9013605a046de86520ee6b"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75b2f0ab341f3c33702ce64e1c101116c7462a25686d0b1a0193ca654ad4f96e"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab29fc61fbd32096b82b02e6b18ae0d7423048d3540b7b90805b16ae10bdb769"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e43741e7453262aa3ba1754623d7864250b33751bd850dd548e3ed6bd1911093"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:155eacc237cb28c9eafda1c47a89e6e4550f1c2e711f2eee21e0bb2f4df75546"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31d7fbe396a51f43375e38c31bc3a96c7996882582f95f3fcfd54acfa7b3ce6"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d3d049186c9966e9140de39a9979d7adcfe22f8b02d2852c94d3c363235cc18"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88728fbd410d61bd5d655ac50b842714c38bc34ff717f73592132d28911fc88e"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:052d302058d72431acc9dd4a9c76854c8dfce10c698deef5252884e32a1ac7bf"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:633110a9431231664be2ad32baf10971547f18289d33967654581b9ae9c94a7e"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f439c9e5297ae29a700f6d55de1525e2e295dbbb7366f0974c8702fca9e536b9"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:94f7eb080c00c0ccbd4fafad69f0b35b624a6a229a28e11d365b60b58a072832"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f876783ec654b7b9525503c2a0a1b086e5d4f52ff65cac7e8747769b0c2e5468"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e039800e50592580171830e788ef4a1d6bb54300d074ae9f9119e92aefc568"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a8cccf0070a29bc01493179db2e61220ee1a6cb17f8ea41c68a2f043ace87f"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f86cef801d7b0838e17b6ee2f2c9e747447d91ad1220a701baccdf7ef11a3c8"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2433a89af4158beddebbdd66fae95b34d40f2467bee8dc40df0333de5e616b5f"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c49d5dc0918ad912777720035f9820bdbb6c7e7d1898e12506d44ab3c938d525"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:331d49bc72430a3f85ea6ecb55a0d0d65f66a21d61af5783b465906a741366d5"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86627bc33bc63b8de869d7d5bfa9868619a4f3e4e7082103935c52f56c66b5"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab6a2ba2706b5eaa1ce6bc7c4e72970bf9587e2e0e482e5fb4df1996bccb7a40"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bca5e7c38d913fabc24ad09545f78ba23cfc13e1ac8250644231729ca908549"}, - {file = "ckzg-1.0.2.tar.gz", hash = "sha256:4295acc380f8d42ebea4a4a0a68c424a322bb335a33bad05c72ead8cbb28d118"}, -] - -[[package]] -name = "cleo" -version = "2.1.0" -description = "Cleo allows you to create beautiful and testable command-line interfaces." -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, - {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, -] - -[package.dependencies] -crashtest = ">=0.4.1,<0.5.0" -rapidfuzz = ">=3.0.0,<4.0.0" - -[[package]] -name = "click" -version = "8.1.8" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.5.3" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "crashtest" -version = "0.4.1" -description = "Manage Python errors with ease" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, - {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, -] - -[[package]] -name = "cryptography" -version = "42.0.8" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, - {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, - {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, - {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, - {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, - {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, - {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, - {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, - {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, - {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, - {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, - {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, - {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, - {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cytoolz" -version = "0.12.3" -description = "Cython implementation of Toolz: High performance functional utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346"}, - {file = "cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98"}, - {file = "cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3"}, - {file = "cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5"}, - {file = "cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd"}, - {file = "cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d"}, - {file = "cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc"}, - {file = "cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389"}, - {file = "cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2"}, - {file = "cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605"}, - {file = "cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716"}, - {file = "cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6"}, - {file = "cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2"}, - {file = "cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887"}, - {file = "cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f"}, - {file = "cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6"}, - {file = "cytoolz-0.12.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9a38332cfad2a91e89405b7c18b3f00e2edc951c225accbc217597d3e4e9fde"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f501ae1353071fa5d6677437bbeb1aeb5622067dce0977cedc2c5ec5843b202"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56f899758146a52e2f8cfb3fb6f4ca19c1e5814178c3d584de35f9e4d7166d91"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800f0526adf9e53d3c6acda748f4def1f048adaa780752f154da5cf22aa488a2"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0976a3fcb81d065473173e9005848218ce03ddb2ec7d40dd6a8d2dba7f1c3ae"}, - {file = "cytoolz-0.12.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c835eab01466cb67d0ce6290601ebef2d82d8d0d0a285ed0d6e46989e4a7a71a"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fba0616fcd487e34b8beec1ad9911d192c62e758baa12fcb44448b9b6feae22"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6f6e8207d732651e0204779e1ba5a4925c93081834570411f959b80681f8d333"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8119bf5961091cfe644784d0bae214e273b3b3a479f93ee3baab97bbd995ccfe"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7ad1331cb68afeec58469c31d944a2100cee14eac221553f0d5218ace1a0b25d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:92c53d508fb8a4463acc85b322fa24734efdc66933a5c8661bdc862103a3373d"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win32.whl", hash = "sha256:2c6dd75dae3d84fa8988861ab8b1189d2488cb8a9b8653828f9cd6126b5e7abd"}, - {file = "cytoolz-0.12.3-cp37-cp37m-win_amd64.whl", hash = "sha256:caf07a97b5220e6334dd32c8b6d8b2bd255ca694eca5dfe914bb5b880ee66cdb"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed0cfb9326747759e2ad81cb6e45f20086a273b67ac3a4c00b19efcbab007c60"}, - {file = "cytoolz-0.12.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:96a5a0292575c3697121f97cc605baf2fd125120c7dcdf39edd1a135798482ca"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b76f2f50a789c44d6fd7f773ec43d2a8686781cd52236da03f7f7d7998989bee"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2905fdccacc64b4beba37f95cab9d792289c80f4d70830b70de2fc66c007ec01"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ebe23028eac51251f22ba01dba6587d30aa9c320372ca0c14eeab67118ec3f"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96c715404a3825e37fe3966fe84c5f8a1f036e7640b2a02dbed96cac0c933451"}, - {file = "cytoolz-0.12.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bac0adffc1b6b6a4c5f1fd1dd2161afb720bcc771a91016dc6bdba59af0a5d3"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:37441bf4a2a4e2e0fe9c3b0ea5e72db352f5cca03903977ffc42f6f6c5467be9"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f04037302049cb30033f7fa4e1d0e44afe35ed6bfcf9b380fc11f2a27d3ed697"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f37b60e66378e7a116931d7220f5352186abfcc950d64856038aa2c01944929c"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ec9be3e4b6f86ea8b294d34c990c99d2ba6c526ef1e8f46f1d52c263d4f32cd7"}, - {file = "cytoolz-0.12.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e9199c9e3fbf380a92b8042c677eb9e7ed4bccb126de5e9c0d26f5888d96788"}, - {file = "cytoolz-0.12.3-cp38-cp38-win32.whl", hash = "sha256:18cd61e078bd6bffe088e40f1ed02001387c29174750abce79499d26fa57f5eb"}, - {file = "cytoolz-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:765b8381d4003ceb1a07896a854eee2c31ebc950a4ae17d1e7a17c2a8feb2a68"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4a52dd2a36b0a91f7aa50ca6c8509057acc481a24255f6cb07b15d339a34e0f"}, - {file = "cytoolz-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:581f1ce479769fe7eeb9ae6d87eadb230df8c7c5fff32138162cdd99d7fb8fc3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f505d4c6eb79585c8ad0b9dc140ef30a138c880e4e3b40230d642690e36366"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59276021619b432a5c21c01cda8320b9cc7dbc40351ffc478b440bfccd5bbdd3"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e44f4c25e1e7cf6149b499c74945a14649c8866d36371a2c2d2164e4649e7755"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64f8e60c1dd69e4d5e615481f2d57937746f4a6be2d0f86e9e7e3b9e2243b5e"}, - {file = "cytoolz-0.12.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c63186f3bf9d7ef1347bc0537bb9a0b4111a0d7d6e619623cabc18fef0dc3b"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fdddb9d988405f24035234f1e8d1653ab2e48cc2404226d21b49a129aefd1d25"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6986632d8a969ea1e720990c818dace1a24c11015fd7c59b9fea0b65ef71f726"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0ba1cbc4d9cd7571c917f88f4a069568e5121646eb5d82b2393b2cf84712cf2a"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7d267ffc9a36c0a9a58c7e0adc9fa82620f22e4a72533e15dd1361f57fc9accf"}, - {file = "cytoolz-0.12.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95e878868a172a41fbf6c505a4b967309e6870e22adc7b1c3b19653d062711fa"}, - {file = "cytoolz-0.12.3-cp39-cp39-win32.whl", hash = "sha256:8e21932d6d260996f7109f2a40b2586070cb0a0cf1d65781e156326d5ebcc329"}, - {file = "cytoolz-0.12.3-cp39-cp39-win_amd64.whl", hash = "sha256:0d8edfbc694af6c9bda4db56643fb8ed3d14e47bec358c2f1417de9a12d6d1fb"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36"}, - {file = "cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cd88028bb897fba99ddd84f253ca6bef73ecb7bdf3f3cf25bc493f8f97d3c7c5"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b19223e7f7bd7a73ec3aa6fdfb73b579ff09c2bc0b7d26857eec2d01a58c76"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a79d72b08048a0980a59457c239555f111ac0c8bdc140c91a025f124104dbb4"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dd70141b32b717696a72b8876e86bc9c6f8eff995c1808e299db3541213ff82"}, - {file = "cytoolz-0.12.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a1445c91009eb775d479e88954c51d0b4cf9a1e8ce3c503c2672d17252882647"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ca6a9a9300d5bda417d9090107c6d2b007683efc59d63cc09aca0e7930a08a85"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6feb903d2a08a4ba2e70e950e862fd3be9be9a588b7c38cee4728150a52918"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b6f43f086e5a965d33d62a145ae121b4ccb6e0789ac0acc895ce084fec8c65"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:534fa66db8564d9b13872d81d54b6b09ae592c585eb826aac235bd6f1830f8ad"}, - {file = "cytoolz-0.12.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:fea649f979def23150680de1bd1d09682da3b54932800a0f90f29fc2a6c98ba8"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a447247ed312dd64e3a8d9483841ecc5338ee26d6e6fbd29cd373ed030db0240"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba3f843aa89f35467b38c398ae5b980a824fdbdb94065adc6ec7c47a0a22f4c7"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:582c22f97a380211fb36a7b65b1beeb84ea11d82015fa84b054be78580390082"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47feb089506fc66e1593cd9ade3945693a9d089a445fbe9a11385cab200b9f22"}, - {file = "cytoolz-0.12.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba9002d2f043943744a9dc8e50a47362bcb6e6f360dc0a1abcb19642584d87bb"}, - {file = "cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282"}, -] - -[package.dependencies] -toolz = ">=0.8.0" - -[package.extras] -cython = ["cython"] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "dulwich" -version = "0.21.7" -description = "Python Git Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d4c0110798099bb7d36a110090f2688050703065448895c4f53ade808d889dd3"}, - {file = "dulwich-0.21.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bc12697f0918bee324c18836053644035362bb3983dc1b210318f2fed1d7132"}, - {file = "dulwich-0.21.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:471305af74790827fcbafe330fc2e8bdcee4fb56ca1177c8c481b1c8f806c4a4"}, - {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54c9d0e845be26f65f954dff13a1cd3f2b9739820c19064257b8fd7435ab263"}, - {file = "dulwich-0.21.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12d61334a575474e707614f2e93d6ed4cdae9eb47214f9277076d9e5615171d3"}, - {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e274cebaf345f0b1e3b70197f2651de92b652386b68020cfd3bf61bc30f6eaaa"}, - {file = "dulwich-0.21.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:817822f970e196e757ae01281ecbf21369383285b9f4a83496312204cf889b8c"}, - {file = "dulwich-0.21.7-cp310-cp310-win32.whl", hash = "sha256:7836da3f4110ce684dcd53489015fb7fa94ed33c5276e3318b8b1cbcb5b71e08"}, - {file = "dulwich-0.21.7-cp310-cp310-win_amd64.whl", hash = "sha256:4a043b90958cec866b4edc6aef5fe3c2c96a664d0b357e1682a46f6c477273c4"}, - {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce8db196e79c1f381469410d26fb1d8b89c6b87a4e7f00ff418c22a35121405c"}, - {file = "dulwich-0.21.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:62bfb26bdce869cd40be443dfd93143caea7089b165d2dcc33de40f6ac9d812a"}, - {file = "dulwich-0.21.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c01a735b9a171dcb634a97a3cec1b174cfbfa8e840156870384b633da0460f18"}, - {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa4d14767cf7a49c9231c2e52cb2a3e90d0c83f843eb6a2ca2b5d81d254cf6b9"}, - {file = "dulwich-0.21.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bca4b86e96d6ef18c5bc39828ea349efb5be2f9b1f6ac9863f90589bac1084d"}, - {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7b5624b02ef808cdc62dabd47eb10cd4ac15e8ac6df9e2e88b6ac6b40133673"}, - {file = "dulwich-0.21.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3a539b4696a42fbdb7412cb7b66a4d4d332761299d3613d90a642923c7560e1"}, - {file = "dulwich-0.21.7-cp311-cp311-win32.whl", hash = "sha256:675a612ce913081beb0f37b286891e795d905691dfccfb9bf73721dca6757cde"}, - {file = "dulwich-0.21.7-cp311-cp311-win_amd64.whl", hash = "sha256:460ba74bdb19f8d498786ae7776745875059b1178066208c0fd509792d7f7bfc"}, - {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4c51058ec4c0b45dc5189225b9e0c671b96ca9713c1daf71d622c13b0ab07681"}, - {file = "dulwich-0.21.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4bc4c5366eaf26dda3fdffe160a3b515666ed27c2419f1d483da285ac1411de0"}, - {file = "dulwich-0.21.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a0650ec77d89cb947e3e4bbd4841c96f74e52b4650830112c3057a8ca891dc2f"}, - {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f18f0a311fb7734b033a3101292b932158cade54b74d1c44db519e42825e5a2"}, - {file = "dulwich-0.21.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c589468e5c0cd84e97eb7ec209ab005a2cb69399e8c5861c3edfe38989ac3a8"}, - {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d62446797163317a397a10080c6397ffaaca51a7804c0120b334f8165736c56a"}, - {file = "dulwich-0.21.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e84cc606b1f581733df4350ca4070e6a8b30be3662bbb81a590b177d0c996c91"}, - {file = "dulwich-0.21.7-cp312-cp312-win32.whl", hash = "sha256:c3d1685f320907a52c40fd5890627945c51f3a5fa4bcfe10edb24fec79caadec"}, - {file = "dulwich-0.21.7-cp312-cp312-win_amd64.whl", hash = "sha256:6bd69921fdd813b7469a3c77bc75c1783cc1d8d72ab15a406598e5a3ba1a1503"}, - {file = "dulwich-0.21.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d8ab29c660125db52106775caa1f8f7f77a69ed1fe8bc4b42bdf115731a25bf"}, - {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0d2e4485b98695bf95350ce9d38b1bb0aaac2c34ad00a0df789aa33c934469b"}, - {file = "dulwich-0.21.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e138d516baa6b5bafbe8f030eccc544d0d486d6819b82387fc0e285e62ef5261"}, - {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f34bf9b9fa9308376263fd9ac43143c7c09da9bc75037bb75c6c2423a151b92c"}, - {file = "dulwich-0.21.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2e2c66888207b71cd1daa2acb06d3984a6bc13787b837397a64117aa9fc5936a"}, - {file = "dulwich-0.21.7-cp37-cp37m-win32.whl", hash = "sha256:10893105c6566fc95bc2a67b61df7cc1e8f9126d02a1df6a8b2b82eb59db8ab9"}, - {file = "dulwich-0.21.7-cp37-cp37m-win_amd64.whl", hash = "sha256:460b3849d5c3d3818a80743b4f7a0094c893c559f678e56a02fff570b49a644a"}, - {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74700e4c7d532877355743336c36f51b414d01e92ba7d304c4f8d9a5946dbc81"}, - {file = "dulwich-0.21.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c92e72c43c9e9e936b01a57167e0ea77d3fd2d82416edf9489faa87278a1cdf7"}, - {file = "dulwich-0.21.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d097e963eb6b9fa53266146471531ad9c6765bf390849230311514546ed64db2"}, - {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:808e8b9cc0aa9ac74870b49db4f9f39a52fb61694573f84b9c0613c928d4caf8"}, - {file = "dulwich-0.21.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1957b65f96e36c301e419d7adaadcff47647c30eb072468901bb683b1000bc5"}, - {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4b09bc3a64fb70132ec14326ecbe6e0555381108caff3496898962c4136a48c6"}, - {file = "dulwich-0.21.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5882e70b74ac3c736a42d3fdd4f5f2e6570637f59ad5d3e684760290b58f041"}, - {file = "dulwich-0.21.7-cp38-cp38-win32.whl", hash = "sha256:29bb5c1d70eba155ded41ed8a62be2f72edbb3c77b08f65b89c03976292f6d1b"}, - {file = "dulwich-0.21.7-cp38-cp38-win_amd64.whl", hash = "sha256:25c3ab8fb2e201ad2031ddd32e4c68b7c03cb34b24a5ff477b7a7dcef86372f5"}, - {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8929c37986c83deb4eb500c766ee28b6670285b512402647ee02a857320e377c"}, - {file = "dulwich-0.21.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc1e11be527ac06316539b57a7688bcb1b6a3e53933bc2f844397bc50734e9ae"}, - {file = "dulwich-0.21.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fc3078a1ba04c588fabb0969d3530efd5cd1ce2cf248eefb6baf7cbc15fc285"}, - {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dcbd29ba30ba2c5bfbab07a61a5f20095541d5ac66d813056c122244df4ac0"}, - {file = "dulwich-0.21.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8869fc8ec3dda743e03d06d698ad489b3705775fe62825e00fa95aa158097fc0"}, - {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d96ca5e0dde49376fbcb44f10eddb6c30284a87bd03bb577c59bb0a1f63903fa"}, - {file = "dulwich-0.21.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0064363bd5e814359657ae32517fa8001e8573d9d040bd997908d488ab886ed"}, - {file = "dulwich-0.21.7-cp39-cp39-win32.whl", hash = "sha256:869eb7be48243e695673b07905d18b73d1054a85e1f6e298fe63ba2843bb2ca1"}, - {file = "dulwich-0.21.7-cp39-cp39-win_amd64.whl", hash = "sha256:404b8edeb3c3a86c47c0a498699fc064c93fa1f8bab2ffe919e8ab03eafaaad3"}, - {file = "dulwich-0.21.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e598d743c6c0548ebcd2baf94aa9c8bfacb787ea671eeeb5828cfbd7d56b552f"}, - {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a2d76c96426e791556836ef43542b639def81be4f1d6d4322cd886c115eae1"}, - {file = "dulwich-0.21.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6c88acb60a1f4d31bd6d13bfba465853b3df940ee4a0f2a3d6c7a0778c705b7"}, - {file = "dulwich-0.21.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ecd315847dea406a4decfa39d388a2521e4e31acde3bd9c2609c989e817c6d62"}, - {file = "dulwich-0.21.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d05d3c781bc74e2c2a2a8f4e4e2ed693540fbe88e6ac36df81deac574a6dad99"}, - {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6de6f8de4a453fdbae8062a6faa652255d22a3d8bce0cd6d2d6701305c75f2b3"}, - {file = "dulwich-0.21.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e25953c7acbbe4e19650d0225af1c0c0e6882f8bddd2056f75c1cc2b109b88ad"}, - {file = "dulwich-0.21.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4637cbd8ed1012f67e1068aaed19fcc8b649bcf3e9e26649826a303298c89b9d"}, - {file = "dulwich-0.21.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:858842b30ad6486aacaa607d60bab9c9a29e7c59dc2d9cb77ae5a94053878c08"}, - {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739b191f61e1c4ce18ac7d520e7a7cbda00e182c3489552408237200ce8411ad"}, - {file = "dulwich-0.21.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:274c18ec3599a92a9b67abaf110e4f181a4f779ee1aaab9e23a72e89d71b2bd9"}, - {file = "dulwich-0.21.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2590e9b431efa94fc356ae33b38f5e64f1834ec3a94a6ac3a64283b206d07aa3"}, - {file = "dulwich-0.21.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed60d1f610ef6437586f7768254c2a93820ccbd4cfdac7d182cf2d6e615969bb"}, - {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8278835e168dd097089f9e53088c7a69c6ca0841aef580d9603eafe9aea8c358"}, - {file = "dulwich-0.21.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffc27fb063f740712e02b4d2f826aee8bbed737ed799962fef625e2ce56e2d29"}, - {file = "dulwich-0.21.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61e3451bd3d3844f2dca53f131982553be4d1b1e1ebd9db701843dd76c4dba31"}, - {file = "dulwich-0.21.7.tar.gz", hash = "sha256:a9e9c66833cea580c3ac12927e4b9711985d76afca98da971405d414de60e968"}, -] - -[package.dependencies] -urllib3 = ">=1.25" - -[package.extras] -fastimport = ["fastimport"] -https = ["urllib3 (>=1.24.1)"] -paramiko = ["paramiko"] -pgp = ["gpg"] - -[[package]] -name = "eth-abi" -version = "5.1.0" -description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_abi-5.1.0-py3-none-any.whl", hash = "sha256:84cac2626a7db8b7d9ebe62b0fdca676ab1014cc7f777189e3c0cd721a4c16d8"}, - {file = "eth_abi-5.1.0.tar.gz", hash = "sha256:33ddd756206e90f7ddff1330cc8cac4aa411a824fe779314a0a52abea2c8fc14"}, -] - -[package.dependencies] -eth-typing = ">=3.0.0" -eth-utils = ">=2.0.0" -parsimonious = ">=0.10.0,<0.11.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] -tools = ["hypothesis (>=4.18.2,<5.0.0)"] - -[[package]] -name = "eth-account" -version = "0.13.0" -description = "eth-account: Sign Ethereum transactions and messages with local private keys" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_account-0.13.0-py3-none-any.whl", hash = "sha256:84d27664038c68e6bee28fa5de803c3b629dc5d97cd61d12e5f442c561a5b8af"}, - {file = "eth_account-0.13.0.tar.gz", hash = "sha256:ccea4383d9d37b46e17c977b0a5ea397cef1ac1ad3330431ae4b0c10b62d4fcd"}, -] - -[package.dependencies] -bitarray = ">=2.4.0" -ckzg = ">=0.4.3" -eth-abi = ">=4.0.0-b.2" -eth-keyfile = ">=0.6.0" -eth-keys = ">=0.4.0" -eth-rlp = ">=2.1.0" -eth-utils = ">=2.0.0" -hexbytes = ">=1.2.0" -pydantic = ">=2.0.0" -rlp = ">=1.0.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "coverage", "hypothesis (>=4.18.0,<5)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["coverage", "hypothesis (>=4.18.0,<5)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-bloom" -version = "3.0.1" -description = "A python implementation of the bloom filter used by Ethereum" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_bloom-3.0.1-py3-none-any.whl", hash = "sha256:c1eb51cb9f9ec8834b691d67e73c02c4e79e22c81ae8058209971803236ffbb2"}, - {file = "eth_bloom-3.0.1.tar.gz", hash = "sha256:6be3dfa44a597a0bc8d974c381fb9a60bbcadfb56e88e69ab55ba538d90b3256"}, -] - -[package.dependencies] -eth-hash = {version = ">=0.4.0", extras = ["pycryptodome"]} - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "hypothesis (>=3.31.2)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=21,<22)"] -test = ["hypothesis (>=3.31.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-hash" -version = "0.7.0" -description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" -optional = false -python-versions = ">=3.8, <4" -files = [ - {file = "eth-hash-0.7.0.tar.gz", hash = "sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a"}, - {file = "eth_hash-0.7.0-py3-none-any.whl", hash = "sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f"}, -] - -[package.dependencies] -pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -pycryptodome = ["pycryptodome (>=3.6.6,<4)"] -pysha3 = ["pysha3 (>=1.0.0,<2.0.0)", "safe-pysha3 (>=1.0.0)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-keyfile" -version = "0.8.1" -description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64"}, - {file = "eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1"}, -] - -[package.dependencies] -eth-keys = ">=0.4.0" -eth-utils = ">=2" -pycryptodome = ">=3.6.6,<4" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=21,<22)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-keys" -version = "0.5.1" -description = "eth-keys: Common API for Ethereum key operations" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_keys-0.5.1-py3-none-any.whl", hash = "sha256:ad13d920a2217a49bed3a1a7f54fb0980f53caf86d3bbab2139fd3330a17b97e"}, - {file = "eth_keys-0.5.1.tar.gz", hash = "sha256:2b587e4bbb9ac2195215a7ab0c0fb16042b17d4ec50240ed670bbb8f53da7a48"}, -] - -[package.dependencies] -eth-typing = ">=3" -eth-utils = ">=2" - -[package.extras] -coincurve = ["coincurve (>=12.0.0)"] -dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bumpversion (>=0.5.3)", "coincurve (>=12.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=21,<22)"] -test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)"] - -[[package]] -name = "eth-rlp" -version = "2.1.0" -description = "eth-rlp: RLP definitions for common Ethereum objects in Python" -optional = false -python-versions = ">=3.8, <4" -files = [ - {file = "eth-rlp-2.1.0.tar.gz", hash = "sha256:d5b408a8cd20ed496e8e66d0559560d29bc21cee482f893936a1f05d0dddc4a0"}, - {file = "eth_rlp-2.1.0-py3-none-any.whl", hash = "sha256:6f476eb7e37d81feaba5d98aed887e467be92648778c44b19fe594aea209cde1"}, -] - -[package.dependencies] -eth-utils = ">=2.0.0" -hexbytes = ">=1.2.0" -rlp = ">=0.6.0" -typing-extensions = {version = ">=4.0.1", markers = "python_version <= \"3.10\""} - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-stdlib" -version = "0.2.7" -description = "Ethereum Standard Library for Python" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "eth_stdlib-0.2.7-py3-none-any.whl", hash = "sha256:d302d8ca8ca14f6137c01ad4e2124a608e98f9132d51b1b7a32748156018d8af"}, - {file = "eth_stdlib-0.2.7.tar.gz", hash = "sha256:9b9c8d709674d4da34014f2ba1f93fc226d90a15f1cc99da033d481131a8feba"}, -] - -[package.dependencies] -pycryptodome = ">=3.18.0,<4.0.0" - -[package.extras] -hypothesis = ["hypothesis (>=6.58.0,<7.0.0)"] - -[[package]] -name = "eth-typing" -version = "4.2.3" -description = "eth-typing: Common type annotations for ethereum python packages" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_typing-4.2.3-py3-none-any.whl", hash = "sha256:b2df49fa89d2e85f2cc3fb1c903b0cd183d524f7a045e3db8cc720cf41adcd3d"}, - {file = "eth_typing-4.2.3.tar.gz", hash = "sha256:8ee3ae7d4136d14fcb955c34f9dbef8e52170984d4dc68c0ab0d61621eab29d8"}, -] - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "eth-utils" -version = "4.1.1" -description = "eth-utils: Common utility functions for python code that interacts with Ethereum" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "eth_utils-4.1.1-py3-none-any.whl", hash = "sha256:ccbbac68a6d65cb6e294c5bcb6c6a5cec79a241c56dc5d9c345ed788c30f8534"}, - {file = "eth_utils-4.1.1.tar.gz", hash = "sha256:71c8d10dec7494aeed20fa7a4d52ec2ce4a2e52fdce80aab4f5c3c19f3648b25"}, -] - -[package.dependencies] -cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} -eth-hash = ">=0.3.1" -eth-typing = ">=3.0.0" -toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.5.1)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["hypothesis (>=4.43.0)", "mypy (==1.5.1)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "exceptiongroup" -version = "1.2.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "executing" -version = "2.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fancycompleter" -version = "0.9.1" -description = "colorful TAB completion for Python prompt" -optional = false -python-versions = "*" -files = [ - {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, - {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, -] - -[package.dependencies] -pyreadline = {version = "*", markers = "platform_system == \"Windows\""} -pyrepl = ">=0.8.2" - -[[package]] -name = "fastjsonschema" -version = "2.19.1" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.19.1-py3-none-any.whl", hash = "sha256:3672b47bc94178c9f23dbb654bf47440155d4db9df5f7bc47643315f9c405cd0"}, - {file = "fastjsonschema-2.19.1.tar.gz", hash = "sha256:e3126a94bdc4623d3de4485f8d468a12f02a67921315ddc87836d6e456dc789d"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "filelock" -version = "3.14.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "ghp-import" -version = "2.1.0" -description = "Copy your docs directly to the gh-pages branch." -optional = false -python-versions = "*" -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[package.dependencies] -python-dateutil = ">=2.8.1" - -[package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] - -[[package]] -name = "hexbytes" -version = "1.2.0" -description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" -optional = false -python-versions = ">=3.8, <4" -files = [ - {file = "hexbytes-1.2.0-py3-none-any.whl", hash = "sha256:bb243ab58b8d8390e3a753fbc9e3616f0f958df43d874e19ae0e4b746722a7e9"}, - {file = "hexbytes-1.2.0.tar.gz", hash = "sha256:965f1cc712e7b263c41fdf3fb36cf671ba6f59b895937cf33941a5c996ec3a5c"}, -] - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "hypothesis" -version = "6.103.1" -description = "A library for property-based testing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "hypothesis-6.103.1-py3-none-any.whl", hash = "sha256:d3c959fab6233e78867499e2117ae9db8dc40eeed936d71a2cfc7b6094972e74"}, - {file = "hypothesis-6.103.1.tar.gz", hash = "sha256:d299d5c21d6408eab3be670c94c974f3acf0b511c61fe81804b09091e393ee1f"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} -sortedcontainers = ">=2.1.0,<3.0.0" - -[package.extras] -all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.54)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] -cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] -codemods = ["libcst (>=0.3.16)"] -crosshair = ["crosshair-tool (>=0.0.54)", "hypothesis-crosshair (>=0.0.4)"] -dateutil = ["python-dateutil (>=1.4)"] -django = ["django (>=3.2)"] -dpcontracts = ["dpcontracts (>=0.4)"] -ghostwriter = ["black (>=19.10b0)"] -lark = ["lark (>=0.10.1)"] -numpy = ["numpy (>=1.17.3)"] -pandas = ["pandas (>=1.1)"] -pytest = ["pytest (>=4.6)"] -pytz = ["pytz (>=2014.1)"] -redis = ["redis (>=3.0.0)"] -zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "importlib-metadata" -version = "7.1.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "installer" -version = "0.7.0" -description = "A library for installing Python wheels." -optional = false -python-versions = ">=3.7" -files = [ - {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, - {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, -] - -[[package]] -name = "ipython" -version = "8.25.0" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.10" -files = [ - {file = "ipython-8.25.0-py3-none-any.whl", hash = "sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab"}, - {file = "ipython-8.25.0.tar.gz", hash = "sha256:c6ed726a140b6e725b911528f80439c534fac915246af3efc39440a6b0f9d716"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5.13.0" -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} - -[package.extras] -all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] -kernel = ["ipykernel"] -matplotlib = ["matplotlib"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -description = "Utility functions for Python class constructs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, - {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, -] - -[package.dependencies] -more-itertools = "*" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jeepney" -version = "0.8.0" -description = "Low-level, pure Python DBus protocol wrapper." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] - -[package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["async_generator", "trio"] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "keyring" -version = "24.3.1" -description = "Store and access your passwords safely." -optional = false -python-versions = ">=3.8" -files = [ - {file = "keyring-24.3.1-py3-none-any.whl", hash = "sha256:df38a4d7419a6a60fea5cef1e45a948a3e8430dd12ad88b0f423c5c143906218"}, - {file = "keyring-24.3.1.tar.gz", hash = "sha256:c3327b6ffafc0e8befbdb597cacdb4928ffe5c1212f7645f186e6d9957a898db"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} -"jaraco.classes" = "*" -jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} -SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} - -[package.extras] -completion = ["shtab (>=1.1.0)"] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "lark" -version = "1.2.2" -description = "a modern parsing library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c"}, - {file = "lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80"}, -] - -[package.extras] -atomic-cache = ["atomicwrites"] -interegular = ["interegular (>=0.3.1,<0.4.0)"] -nearley = ["js2py"] -regex = ["regex"] - -[[package]] -name = "lru-dict" -version = "1.3.0" -description = "An Dict like LRU container." -optional = false -python-versions = ">=3.8" -files = [ - {file = "lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4073333894db9840f066226d50e6f914a2240711c87d60885d8c940b69a6673f"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0ad6361e4dd63b47b2fc8eab344198f37387e1da3dcfacfee19bafac3ec9f1eb"}, - {file = "lru_dict-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c637ab54b8cd9802fe19b260261e38820d748adf7606e34045d3c799b6dde813"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fce5f95489ca1fc158cc9fe0f4866db9cec82c2be0470926a9080570392beaf"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2bf2e24cf5f19c3ff69bf639306e83dced273e6fa775b04e190d7f5cd16f794"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90059f7701bef3c4da073d6e0434a9c7dc551d5adce30e6b99ef86b186f4b4a"}, - {file = "lru_dict-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ecb7ae557239c64077e9b26a142eb88e63cddb104111a5122de7bebbbd00098"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6af36166d22dba851e06a13e35bbf33845d3dd88872e6aebbc8e3e7db70f4682"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ee38d420c77eed548df47b7d74b5169a98e71c9e975596e31ab808e76d11f09"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0e1845024c31e6ff246c9eb5e6f6f1a8bb564c06f8a7d6d031220044c081090b"}, - {file = "lru_dict-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ca5474b1649555d014be1104e5558a92497509021a5ba5ea6e9b492303eb66b"}, - {file = "lru_dict-1.3.0-cp310-cp310-win32.whl", hash = "sha256:ebb03a9bd50c2ed86d4f72a54e0aae156d35a14075485b2127c4b01a3f4a63fa"}, - {file = "lru_dict-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:04cda617f4e4c27009005d0a8185ef02829b14b776d2791f5c994cc9d668bc24"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51"}, - {file = "lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd"}, - {file = "lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e"}, - {file = "lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a"}, - {file = "lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4"}, - {file = "lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549"}, - {file = "lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6"}, - {file = "lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4"}, - {file = "lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df"}, - {file = "lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc"}, - {file = "lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c265f16c936a8ff3bb4b8a4bda0be94c15ec28b63e99fdb1439c1ffe4cd437db"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:784ca9d3b0730b3ec199c0a58f66264c63dd5d438119c739c349a6a9be8e5f6e"}, - {file = "lru_dict-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e13b2f58f647178470adaa14603bb64cc02eeed32601772ccea30e198252883c"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ffbce5c2e80f57937679553c8f27e61ec327c962bf7ea0b15f1d74277fd5363"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7969cb034b3ccc707aff877c73c225c32d7e2a7981baa8f92f5dd4d468fe8c33"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9ab676609cce85dd65d91c275e47da676d13d77faa72de286fbea30fbaa596"}, - {file = "lru_dict-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27c078b5d75989952acbf9b77e14c3dadc468a4aafe85174d548afbc5efc38b"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6123aefe97762ad74215d05320a7f389f196f0594c8813534284d4eafeca1a96"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cd869cadba9a63e1e7fe2dced4a5747d735135b86016b0a63e8c9e324ab629ac"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:40a8daddc29c7edb09dfe44292cf111f1e93a8344349778721d430d336b50505"}, - {file = "lru_dict-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a03170e4152836987a88dcebde61aaeb73ab7099a00bb86509d45b3fe424230"}, - {file = "lru_dict-1.3.0-cp38-cp38-win32.whl", hash = "sha256:3b4f121afe10f5a82b8e317626eb1e1c325b3f104af56c9756064cd833b1950b"}, - {file = "lru_dict-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:1470f5828c7410e16c24b5150eb649647986e78924816e6fb0264049dea14a2b"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c9f746a9917e784fffcedeac4c8c47a3dbd90cbe13b69e9140182ad97ce4b7"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2789296819525a1f3204072dfcf3df6db8bcf69a8fc740ffd3de43a684ea7002"}, - {file = "lru_dict-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:170b66d29945391460351588a7bd8210a95407ae82efe0b855e945398a1d24ea"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774ca88501a9effe8797c3db5a6685cf20978c9cb0fe836b6813cfe1ca60d8c9"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df2e119c6ae412d2fd641a55f8a1e2e51f45a3de3449c18b1b86c319ab79e0c4"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28aa1ea42a7e48174bf513dc2416fea7511a547961e678dc6f5670ca987c18cb"}, - {file = "lru_dict-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9537e1cee6fa582cb68f2fb9ce82d51faf2ccc0a638b275d033fdcb1478eb80b"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:64545fca797fe2c68c5168efb5f976c6e1459e058cab02445207a079180a3557"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a193a14c66cfc0c259d05dddc5e566a4b09e8f1765e941503d065008feebea9d"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3cb1de0ce4137b060abaafed8474cc0ebd12cedd88aaa7f7b3ebb1ddfba86ae0"}, - {file = "lru_dict-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8551ccab1349d4bebedab333dfc8693c74ff728f4b565fe15a6bf7d296bd7ea9"}, - {file = "lru_dict-1.3.0-cp39-cp39-win32.whl", hash = "sha256:6cb0be5e79c3f34d69b90d8559f0221e374b974b809a22377122c4b1a610ff67"}, - {file = "lru_dict-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9f725f2a0bdf1c18735372d5807af4ea3b77888208590394d4660e3d07971f21"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f8f7824db5a64581180ab9d09842e6dd9fcdc46aac9cb592a0807cd37ea55680"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd04b7e7b0c0c192d738df9c317093335e7282c64c9d1bb6b7ebb54674b4e24"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5c20f236f27551e3f0adbf1a987673fb1e9c38d6d284502cd38f5a3845ef681"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca3703ff03b03a1848c563bc2663d0ad813c1cd42c4d9cf75b623716d4415d9a"}, - {file = "lru_dict-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a9fb71ba262c6058a0017ce83d343370d0a0dbe2ae62c2eef38241ec13219330"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f5b88a7c39e307739a3701194993455968fcffe437d1facab93546b1b8a334c1"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2682bfca24656fb7a643621520d57b7fe684ed5fa7be008704c1235d38e16a32"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96fc87ddf569181827458ec5ad8fa446c4690cffacda66667de780f9fcefd44d"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcec98e2c7da7631f0811730303abc4bdfe70d013f7a11e174a2ccd5612a7c59"}, - {file = "lru_dict-1.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6bba2863060caeaedd8386b0c8ee9a7ce4d57a7cb80ceeddf440b4eff2d013ba"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c497fb60279f1e1d7dfbe150b1b069eaa43f7e172dab03f206282f4994676c5"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9509d817a47597988615c1a322580c10100acad10c98dfcf3abb41e0e5877f"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0213ab4e3d9a8d386c18e485ad7b14b615cb6f05df6ef44fb2a0746c6ea9278b"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b50fbd69cd3287196796ab4d50e4cc741eb5b5a01f89d8e930df08da3010c385"}, - {file = "lru_dict-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5247d1f011f92666010942434020ddc5a60951fefd5d12a594f0e5d9f43e3b3b"}, -] - -[package.extras] -test = ["pytest"] - -[[package]] -name = "markdown" -version = "3.8" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.9" -files = [ - {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, - {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, -] - -[package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] -testing = ["coverage", "pyyaml"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} -ghp-import = ">=1.0" -jinja2 = ">=2.11.1" -markdown = ">=3.3.6" -markupsafe = ">=2.0.1" -mergedeep = ">=1.3.4" -mkdocs-get-deps = ">=0.2.0" -packaging = ">=20.5" -pathspec = ">=0.11.1" -pyyaml = ">=5.1" -pyyaml-env-tag = ">=0.1" -watchdog = ">=2.0" - -[package.extras] -i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[package.dependencies] -mergedeep = ">=1.3.4" -platformdirs = ">=2.2.0" -pyyaml = ">=5.1" - -[[package]] -name = "mkdocs-material" -version = "9.5.41" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material-9.5.41-py3-none-any.whl", hash = "sha256:990bc138c33342b5b73e7545915ebc0136e501bfbd8e365735144f5120891d83"}, - {file = "mkdocs_material-9.5.41.tar.gz", hash = "sha256:30fa5d459b4b8130848ecd8e1c908878345d9d8268f7ddbc31eebe88d462d97b"}, -] - -[package.dependencies] -babel = ">=2.10,<3.0" -colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" -mkdocs = ">=1.6,<2.0" -mkdocs-material-extensions = ">=1.3,<2.0" -paginate = ">=0.5,<1.0" -pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -regex = ">=2022.4" -requests = ">=2.26,<3.0" - -[package.extras] -git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] -imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] -recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -description = "Extension pack for Python Markdown and MkDocs Material." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "more-itertools" -version = "10.2.0" -description = "More routines for operating on iterables, beyond itertools" -optional = false -python-versions = ">=3.8" -files = [ - {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, - {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, -] - -[[package]] -name = "msgpack" -version = "1.0.8" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "paginate" -version = "0.5.7" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[package.extras] -dev = ["pytest", "tox"] -lint = ["black"] - -[[package]] -name = "parsimonious" -version = "0.10.0" -description = "(Soon to be) the fastest pure-Python PEG parser I could muster" -optional = false -python-versions = "*" -files = [ - {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, - {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, -] - -[package.dependencies] -regex = ">=2022.3.15" - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pdbpp" -version = "0.10.3" -description = "pdb++, a drop-in replacement for pdb" -optional = false -python-versions = "*" -files = [ - {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, - {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, -] - -[package.dependencies] -fancycompleter = ">=0.8" -pygments = "*" -wmctrl = "*" - -[package.extras] -funcsigs = ["funcsigs"] -testing = ["funcsigs", "pytest"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pkginfo" -version = "1.11.0" -description = "Query metadata from sdists / bdists / installed packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pkginfo-1.11.0-py3-none-any.whl", hash = "sha256:6d4998d1cd42c297af72cc0eab5f5bab1d356fb8a55b828fa914173f8bc1ba05"}, - {file = "pkginfo-1.11.0.tar.gz", hash = "sha256:dba885aa82e31e80d615119874384923f4e011c2a39b0c4b7104359e36cb7087"}, -] - -[package.extras] -testing = ["pytest", "pytest-cov", "wheel"] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "poetry" -version = "1.8.3" -description = "Python dependency management and packaging made easy." -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "poetry-1.8.3-py3-none-any.whl", hash = "sha256:88191c69b08d06f9db671b793d68f40048e8904c0718404b63dcc2b5aec62d13"}, - {file = "poetry-1.8.3.tar.gz", hash = "sha256:67f4eb68288eab41e841cc71a00d26cf6bdda9533022d0189a145a34d0a35f48"}, -] - -[package.dependencies] -build = ">=1.0.3,<2.0.0" -cachecontrol = {version = ">=0.14.0,<0.15.0", extras = ["filecache"]} -cleo = ">=2.1.0,<3.0.0" -crashtest = ">=0.4.1,<0.5.0" -dulwich = ">=0.21.2,<0.22.0" -fastjsonschema = ">=2.18.0,<3.0.0" -installer = ">=0.7.0,<0.8.0" -keyring = ">=24.0.0,<25.0.0" -packaging = ">=23.1" -pexpect = ">=4.7.0,<5.0.0" -pkginfo = ">=1.10,<2.0" -platformdirs = ">=3.0.0,<5" -poetry-core = "1.9.0" -poetry-plugin-export = ">=1.6.0,<2.0.0" -pyproject-hooks = ">=1.0.0,<2.0.0" -requests = ">=2.26,<3.0" -requests-toolbelt = ">=1.0.0,<2.0.0" -shellingham = ">=1.5,<2.0" -tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.11.4,<1.0.0" -trove-classifiers = ">=2022.5.19" -virtualenv = ">=20.23.0,<21.0.0" -xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} - -[[package]] -name = "poetry-core" -version = "1.9.0" -description = "Poetry PEP 517 Build Backend" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "poetry_core-1.9.0-py3-none-any.whl", hash = "sha256:4e0c9c6ad8cf89956f03b308736d84ea6ddb44089d16f2adc94050108ec1f5a1"}, - {file = "poetry_core-1.9.0.tar.gz", hash = "sha256:fa7a4001eae8aa572ee84f35feb510b321bd652e5cf9293249d62853e1f935a2"}, -] - -[[package]] -name = "poetry-plugin-export" -version = "1.8.0" -description = "Poetry plugin to export the dependencies to various formats" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "poetry_plugin_export-1.8.0-py3-none-any.whl", hash = "sha256:adbe232cfa0cc04991ea3680c865cf748bff27593b9abcb1f35fb50ed7ba2c22"}, - {file = "poetry_plugin_export-1.8.0.tar.gz", hash = "sha256:1fa6168a85d59395d835ca564bc19862a7c76061e60c3e7dfaec70d50937fc61"}, -] - -[package.dependencies] -poetry = ">=1.8.0,<3.0.0" -poetry-core = ">=1.7.0,<3.0.0" - -[[package]] -name = "prompt-toolkit" -version = "3.0.46" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.46-py3-none-any.whl", hash = "sha256:45abe60a8300f3c618b23c16c4bb98c6fc80af8ce8b17c7ae92db48db3ee63c1"}, - {file = "prompt_toolkit-3.0.46.tar.gz", hash = "sha256:869c50d682152336e23c4db7f74667639b5047494202ffe7670817053fd57795"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - -[[package]] -name = "py-ecc" -version = "7.0.1" -description = "py-ecc: Elliptic curve crypto in python including secp256k1, alt_bn128, and bls12_381" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "py_ecc-7.0.1-py3-none-any.whl", hash = "sha256:84a8b4d436163c83c65345a68e32f921ef6e64374a36f8e561f0455b4b08f5f2"}, - {file = "py_ecc-7.0.1.tar.gz", hash = "sha256:557461f42e57294d734305a30faf6b8903421651871e9cdeff8d8e67c6796c70"}, -] - -[package.dependencies] -cached-property = ">=1.5.1" -eth-typing = ">=3.0.0" -eth-utils = ">=2.0.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "py-evm" -version = "0.10.1b1" -description = "Python implementation of the Ethereum Virtual Machine" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "py_evm-0.10.1b1-py3-none-any.whl", hash = "sha256:f0fc4a4b904917b40e6a06f87925017dc48ea6582e95f88d28be38f3566e2bae"}, - {file = "py_evm-0.10.1b1.tar.gz", hash = "sha256:aeb889514af12b6a8cb5091fe93008642eadf7c19999859dad3191eaf451647c"}, -] - -[package.dependencies] -cached-property = ">=1.5.1" -ckzg = ">=0.4.3" -eth-bloom = ">=1.0.3" -eth-keys = ">=0.4.0" -eth-typing = ">=3.3.0" -eth-utils = ">=2.0.0" -lru-dict = ">=1.1.6" -py-ecc = ">=1.4.7" -rlp = ">=3.0.0" -trie = ">=2.0.0" - -[package.extras] -benchmark = ["termcolor (>=1.1.0)", "web3 (>=6.0.0)"] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "cached-property (>=1.5.1)", "ckzg (>=0.4.3)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=3.3.0)", "eth-utils (>=2.0.0)", "factory-boy (>=3.0.0)", "hypothesis (>=6,<7)", "ipython", "lru-dict (>=1.1.6)", "pre-commit (>=3.4.0)", "py-ecc (>=1.4.7)", "py-evm (>=0.8.0b1)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.20.0)", "pytest-cov (>=4.0.0)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=3.0)", "rlp (>=3.0.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "trie (>=2.0.0)", "twine", "wheel"] -docs = ["py-evm (>=0.8.0b1)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "sphinxcontrib-asyncio (>=0.2.0)", "towncrier (>=21,<22)"] -eth = ["cached-property (>=1.5.1)", "ckzg (>=0.4.3)", "eth-bloom (>=1.0.3)", "eth-keys (>=0.4.0)", "eth-typing (>=3.3.0)", "eth-utils (>=2.0.0)", "lru-dict (>=1.1.6)", "py-ecc (>=1.4.7)", "rlp (>=3.0.0)", "trie (>=2.0.0)"] -eth-extra = ["blake2b-py (>=0.2.0)", "coincurve (>=18.0.0)"] -test = ["factory-boy (>=3.0.0)", "hypothesis (>=6,<7)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.20.0)", "pytest-cov (>=4.0.0)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=3.0)"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pycryptodome" -version = "3.20.0" -description = "Cryptographic library for Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, -] - -[[package]] -name = "pydantic" -version = "2.7.3" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, - {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.4" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.18.4" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, - {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, - {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, - {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, - {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, - {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, - {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, - {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, - {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, - {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, - {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, - {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, - {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, - {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, - {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, - {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, - {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, - {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, - {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, - {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, - {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, - {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, - {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, - {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"}, - {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"}, - {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"}, - {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"}, - {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"}, - {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"}, - {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"}, - {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, - {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, - {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, - {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, - {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, - {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, - {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, - {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, - {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, - {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymdown-extensions" -version = "10.14.3" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.19.1)"] - -[[package]] -name = "pyproject-hooks" -version = "1.1.0" -description = "Wrappers to call pyproject.toml-based build backend hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, - {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, -] - -[[package]] -name = "pyreadline" -version = "2.1" -description = "A python implmementation of GNU readline." -optional = false -python-versions = "*" -files = [ - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, -] - -[[package]] -name = "pyrepl" -version = "0.9.0" -description = "A library for building flexible command line interfaces" -optional = false -python-versions = "*" -files = [ - {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, -] - -[[package]] -name = "pytest" -version = "8.2.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2.0" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-forked" -version = "1.6.0" -description = "run tests in isolated forked subprocesses" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f"}, - {file = "pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0"}, -] - -[package.dependencies] -py = "*" -pytest = ">=3.10" - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "pywin32-ctypes" -version = "0.2.2" -description = "A (partial) reimplementation of pywin32 using ctypes/cffi" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, - {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[package.dependencies] -pyyaml = "*" - -[[package]] -name = "rapidfuzz" -version = "3.9.3" -description = "rapid fuzzy string matching" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdb8c5b8e29238ec80727c2ba3b301efd45aa30c6a7001123a6647b8e6f77ea4"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3bd0d9632088c63a241f217742b1cf86e2e8ae573e01354775bd5016d12138c"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153f23c03d4917f6a1fc2fb56d279cc6537d1929237ff08ee7429d0e40464a18"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96c5225e840f1587f1bac8fa6f67562b38e095341576e82b728a82021f26d62"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b777cd910ceecd738adc58593d6ed42e73f60ad04ecdb4a841ae410b51c92e0e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53e06e4b81f552da04940aa41fc556ba39dee5513d1861144300c36c33265b76"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7ca5b6050f18fdcacdada2dc5fb7619ff998cd9aba82aed2414eee74ebe6cd"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:87bb8d84cb41446a808c4b5f746e29d8a53499381ed72f6c4e456fe0f81c80a8"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:959a15186d18425d19811bea86a8ffbe19fd48644004d29008e636631420a9b7"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a24603dd05fb4e3c09d636b881ce347e5f55f925a6b1b4115527308a323b9f8e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d055da0e801c71dd74ba81d72d41b2fa32afa182b9fea6b4b199d2ce937450d"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:875b581afb29a7213cf9d98cb0f98df862f1020bce9d9b2e6199b60e78a41d14"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win32.whl", hash = "sha256:6073a46f61479a89802e3f04655267caa6c14eb8ac9d81a635a13805f735ebc1"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:119c010e20e561249b99ca2627f769fdc8305b07193f63dbc07bca0a6c27e892"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:790b0b244f3213581d42baa2fed8875f9ee2b2f9b91f94f100ec80d15b140ba9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f57e8305c281e8c8bc720515540e0580355100c0a7a541105c6cafc5de71daae"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4fc7b784cf987dbddc300cef70e09a92ed1bce136f7bb723ea79d7e297fe76d"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b422c0a6fe139d5447a0766268e68e6a2a8c2611519f894b1f31f0a392b9167"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f50fed4a9b0c9825ff37cf0bccafd51ff5792090618f7846a7650f21f85579c9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b80eb7cbe62348c61d3e67e17057cddfd6defab168863028146e07d5a8b24a89"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f45be77ec82da32ce5709a362e236ccf801615cc7163b136d1778cf9e31b14"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd84b7f652a5610733400307dc732f57c4a907080bef9520412e6d9b55bc9adc"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e6d27dad8c990218b8cd4a5c99cbc8834f82bb46ab965a7265d5aa69fc7ced7"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05ee0696ebf0dfe8f7c17f364d70617616afc7dafe366532730ca34056065b8a"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2bc8391749e5022cd9e514ede5316f86e332ffd3cfceeabdc0b17b7e45198a8c"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93981895602cf5944d89d317ae3b1b4cc684d175a8ae2a80ce5b65615e72ddd0"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:754b719a4990735f66653c9e9261dcf52fd4d925597e43d6b9069afcae700d21"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win32.whl", hash = "sha256:14c9f268ade4c88cf77ab007ad0fdf63699af071ee69378de89fff7aa3cae134"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc1991b4cde6c9d3c0bbcb83d5581dc7621bec8c666c095c65b4277233265a82"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:0c34139df09a61b1b557ab65782ada971b4a3bce7081d1b2bee45b0a52231adb"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d6a210347d6e71234af5c76d55eeb0348b026c9bb98fe7c1cca89bac50fb734"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b300708c917ce52f6075bdc6e05b07c51a085733650f14b732c087dc26e0aaad"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83ea7ca577d76778250421de61fb55a719e45b841deb769351fc2b1740763050"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8319838fb5b7b5f088d12187d91d152b9386ce3979ed7660daa0ed1bff953791"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:505d99131afd21529293a9a7b91dfc661b7e889680b95534756134dc1cc2cd86"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c52970f7784518d7c82b07a62a26e345d2de8c2bd8ed4774e13342e4b3ff4200"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:143caf7247449055ecc3c1e874b69e42f403dfc049fc2f3d5f70e1daf21c1318"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b8ab0fa653d9225195a8ff924f992f4249c1e6fa0aea563f685e71b81b9fcccf"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57e7c5bf7b61c7320cfa5dde1e60e678d954ede9bb7da8e763959b2138391401"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51fa1ba84653ab480a2e2044e2277bd7f0123d6693051729755addc0d015c44f"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:17ff7f7eecdb169f9236e3b872c96dbbaf116f7787f4d490abd34b0116e3e9c8"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afe7c72d3f917b066257f7ff48562e5d462d865a25fbcabf40fca303a9fa8d35"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win32.whl", hash = "sha256:e53ed2e9b32674ce96eed80b3b572db9fd87aae6742941fb8e4705e541d861ce"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:35b7286f177e4d8ba1e48b03612f928a3c4bdac78e5651379cec59f95d8651e6"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:e6e4b9380ed4758d0cb578b0d1970c3f32dd9e87119378729a5340cb3169f879"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a39890013f6d5b056cc4bfdedc093e322462ece1027a57ef0c636537bdde7531"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b5bc0fdbf419493163c5c9cb147c5fbe95b8e25844a74a8807dcb1a125e630cf"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe6e200a75a792d37b960457904c4fce7c928a96ae9e5d21d2bd382fe39066e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de077c468c225d4c18f7188c47d955a16d65f21aab121cbdd98e3e2011002c37"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f917eaadf5388466a95f6a236f678a1588d231e52eda85374077101842e794e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858ba57c05afd720db8088a8707079e8d024afe4644001fe0dbd26ef7ca74a65"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36447d21b05f90282a6f98c5a33771805f9222e5d0441d03eb8824e33e5bbb4"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:acbe4b6f1ccd5b90c29d428e849aa4242e51bb6cab0448d5f3c022eb9a25f7b1"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:53c7f27cdf899e94712972237bda48cfd427646aa6f5d939bf45d084780e4c16"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6175682a829c6dea4d35ed707f1dadc16513270ef64436568d03b81ccb6bdb74"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5276df395bd8497397197fca2b5c85f052d2e6a66ffc3eb0544dd9664d661f95"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:77b5c4f3e72924d7845f0e189c304270066d0f49635cf8a3938e122c437e58de"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win32.whl", hash = "sha256:8add34061e5cd561c72ed4febb5c15969e7b25bda2bb5102d02afc3abc1f52d0"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:604e0502a39cf8e67fa9ad239394dddad4cdef6d7008fdb037553817d420e108"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21047f55d674614eb4b0ab34e35c3dc66f36403b9fbfae645199c4a19d4ed447"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a56da3aff97cb56fe85d9ca957d1f55dbac7c27da927a86a2a86d8a7e17f80aa"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c08481aec2fe574f0062e342924db2c6b321391aeb73d68853ed42420fd6d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e2b827258beefbe5d3f958243caa5a44cf46187eff0c20e0b2ab62d1550327a"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6e65a301fcd19fbfbee3a514cc0014ff3f3b254b9fd65886e8a9d6957fb7bca"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe93ba1725a8d47d2b9dca6c1f435174859427fbc054d83de52aea5adc65729"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca21c0a34adee582775da997a600283e012a608a107398d80a42f9a57ad323d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:256e07d3465173b2a91c35715a2277b1ee3ae0b9bbab4e519df6af78570741d0"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:802ca2cc8aa6b8b34c6fdafb9e32540c1ba05fca7ad60b3bbd7ec89ed1797a87"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:dd789100fc852cffac1449f82af0da139d36d84fd9faa4f79fc4140a88778343"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d0abbacdb06e27ff803d7ae0bd0624020096802758068ebdcab9bd49cf53115"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:378d1744828e27490a823fc6fe6ebfb98c15228d54826bf4e49e4b76eb5f5579"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win32.whl", hash = "sha256:5d0cb272d43e6d3c0dedefdcd9d00007471f77b52d2787a4695e9dd319bb39d2"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:15e4158ac4b3fb58108072ec35b8a69165f651ba1c8f43559a36d518dbf9fb3f"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_arm64.whl", hash = "sha256:58c6a4936190c558d5626b79fc9e16497e5df7098589a7e80d8bff68148ff096"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5410dc848c947a603792f4f51b904a3331cf1dc60621586bfbe7a6de72da1091"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:282d55700a1a3d3a7980746eb2fcd48c9bbc1572ebe0840d0340d548a54d01fe"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc1037507810833646481f5729901a154523f98cbebb1157ba3a821012e16402"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e33f779391caedcba2ba3089fb6e8e557feab540e9149a5c3f7fea7a3a7df37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41a81a9f311dc83d22661f9b1a1de983b201322df0c4554042ffffd0f2040c37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a93250bd8fae996350c251e1752f2c03335bb8a0a5b0c7e910a593849121a435"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3617d1aa7716c57d120b6adc8f7c989f2d65bc2b0cbd5f9288f1fc7bf469da11"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad04a3f5384b82933213bba2459f6424decc2823df40098920856bdee5fd6e88"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8709918da8a88ad73c9d4dd0ecf24179a4f0ceba0bee21efc6ea21a8b5290349"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b770f85eab24034e6ef7df04b2bfd9a45048e24f8a808e903441aa5abde8ecdd"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930b4e6fdb4d914390141a2b99a6f77a52beacf1d06aa4e170cba3a98e24c1bc"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c8444e921bfc3757c475c4f4d7416a7aa69b2d992d5114fe55af21411187ab0d"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c1d3ef3878f871abe6826e386c3d61b5292ef5f7946fe646f4206b85836b5da"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d861bf326ee7dabc35c532a40384541578cd1ec1e1b7db9f9ecbba56eb76ca22"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cde6b9d9ba5007077ee321ec722fa714ebc0cbd9a32ccf0f4dd3cc3f20952d71"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb6546e7b6bed1aefbe24f68a5fb9b891cc5aef61bca6c1a7b1054b7f0359bb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8a57261ef7996d5ced7c8cba9189ada3fbeffd1815f70f635e4558d93766cb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:67201c02efc596923ad950519e0b75ceb78d524177ea557134d6567b9ac2c283"}, - {file = "rapidfuzz-3.9.3.tar.gz", hash = "sha256:b398ea66e8ed50451bce5997c430197d5e4b06ac4aa74602717f792d8d8d06e2"}, -] - -[package.extras] -full = ["numpy"] - -[[package]] -name = "regex" -version = "2024.5.15" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, - {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, - {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, - {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, - {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, - {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, - {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, - {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, - {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, - {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, - {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, - {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rlp" -version = "4.0.1" -description = "rlp: A package for Recursive Length Prefix encoding and decoding" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "rlp-4.0.1-py3-none-any.whl", hash = "sha256:ff6846c3c27b97ee0492373aa074a7c3046aadd973320f4fffa7ac45564b0258"}, - {file = "rlp-4.0.1.tar.gz", hash = "sha256:bcefb11013dfadf8902642337923bd0c786dc8a27cb4c21da6e154e52869ecb1"}, -] - -[package.dependencies] -eth-utils = ">=2" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "hypothesis (==5.19.0)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -rust-backend = ["rusty-rlp (>=0.2.1)"] -test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "secretstorage" -version = "3.3.3" -description = "Python bindings to FreeDesktop.org Secret Service API" -optional = false -python-versions = ">=3.6" -files = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] - -[package.dependencies] -cryptography = ">=2.0" -jeepney = ">=0.6" - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "snekmate" -version = "0.1.1" -description = "State-of-the-art, highly opinionated, hyper-optimised, and secure 🐍Vyper smart contract building blocks." -optional = false -python-versions = ">=3.10" -files = [ - {file = "snekmate-0.1.1-py3-none-any.whl", hash = "sha256:694dc3e5e1e7e5b82433852c303fa0deacb683cfc26a1f9e0505d90ab7a610a9"}, - {file = "snekmate-0.1.1.tar.gz", hash = "sha256:7374e28f394466b6b545b76f4567b8b264684e444552031cd7b013e93b0900eb"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "titanoboa" -version = "0.2.6" -description = "A Vyper interpreter" -optional = false -python-versions = "*" -files = [ - {file = "titanoboa-0.2.6-py3-none-any.whl", hash = "sha256:63bee3b97428697fa2f269c2d2ecbc763b372dc774e3f6229c90f32827bd1967"}, - {file = "titanoboa-0.2.6.tar.gz", hash = "sha256:6bbf0063bfc5d7ffab5044da53e23564fcb4cf5cfe39b1e31a2c01df8ccb96e0"}, -] - -[package.dependencies] -eth-abi = "*" -eth-account = ">=0.13.0" -eth-stdlib = ">=0.2.7,<0.3.0" -eth-typing = "*" -hypothesis = "*" -mkdocs-material = "9.5.41" -py-evm = ">=0.10.0b4" -pytest = "*" -pytest-cov = "*" -requests = "*" -rich = "*" -typing-extensions = "*" -vvm = ">=0.3.2" -vyper = ">=0.4.1" - -[package.extras] -colab = ["ipykernel (>=6.29.4)"] -forking-recommended = ["requests-cache (>=1.2.1)", "ujson (>=5.10.0)"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.5" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, -] - -[[package]] -name = "toolz" -version = "0.12.1" -description = "List processing tools and functional utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "trie" -version = "3.0.1" -description = "Python implementation of the Ethereum Trie structure" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "trie-3.0.1-py3-none-any.whl", hash = "sha256:fbe90011a28f4fc6597bc83706589c2a74c81c8b1410c5e16eebfae0e9796464"}, - {file = "trie-3.0.1.tar.gz", hash = "sha256:3f53adaa04726eb23cb786b0118e62d1f2fb6ed0a7968be964dc34aba27380ee"}, -] - -[package.dependencies] -eth-hash = ">=0.1.0" -eth-utils = ">=2.0.0" -hexbytes = ">=0.2.3" -rlp = ">=3" -sortedcontainers = ">=2.1.0" - -[package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-hash (>=0.1.0,<1.0.0)", "hypothesis (>=6.56.4,<7)", "ipython", "pre-commit (>=3.4.0)", "pycryptodome", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["towncrier (>=21,<22)"] -test = ["hypothesis (>=6.56.4,<7)", "pycryptodome", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] - -[[package]] -name = "trove-classifiers" -version = "2024.5.22" -description = "Canonical source for classifiers on PyPI (pypi.org)." -optional = false -python-versions = "*" -files = [ - {file = "trove_classifiers-2024.5.22-py3-none-any.whl", hash = "sha256:c43ade18704823e4afa3d9db7083294bc4708a5e02afbcefacd0e9d03a7a24ef"}, - {file = "trove_classifiers-2024.5.22.tar.gz", hash = "sha256:8a6242bbb5c9ae88d34cf665e816b287d2212973c8777dfaef5ec18d72ac1d03"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.1" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, -] - -[[package]] -name = "urllib3" -version = "2.2.1" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.26.2" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "vvm" -version = "0.3.2" -description = "Vyper version management tool" -optional = false -python-versions = "<4,>=3.8" -files = [ - {file = "vvm-0.3.2-py3-none-any.whl", hash = "sha256:f9f4ccad6c9a8ff6978285f005fbb086777baa160e6132d454c35d66bc94d017"}, - {file = "vvm-0.3.2.tar.gz", hash = "sha256:939838e6417923ef20c48d1a5f9d9fcb39e963c7e0152aaf99c966a6c332e770"}, -] - -[package.dependencies] -packaging = ">=23.1,<25" -requests = ">=2.32.3,<3" - -[[package]] -name = "vyper" -version = "0.4.1" -description = "Vyper: the Pythonic Programming Language for the EVM" -optional = false -python-versions = "<4,>=3.10" -files = [ - {file = "vyper-0.4.1-py3-none-any.whl", hash = "sha256:b22e19e20557b3c13fa2721efabc4e926f624af9a3bd328e1da8fb6b727d908f"}, - {file = "vyper-0.4.1.tar.gz", hash = "sha256:2a219b895c9b5ad6a71237a6fb7d03a6eccaa800faa30ba32418847e350a95e3"}, -] - -[package.dependencies] -asttokens = ">=2.0.5,<3" -cbor2 = ">=5.4.6,<6" -importlib-metadata = "*" -lark = ">=1.0.0,<2" -packaging = ">=23.1,<24" -pycryptodome = ">=3.5.1,<4" -wheel = "*" - -[package.extras] -dev = ["black (==23.12.0)", "eth-abi (>=5.0.0,<6.0.0)", "eth-account (==0.12.2)", "eth-stdlib (==0.2.7)", "flake8 (==6.1.0)", "flake8-bugbear (==23.12.2)", "flake8-use-fstring (==1.4)", "hexbytes (>=1.2)", "hypothesis[lark] (>=6.0,<7.0)", "ipython", "isort (==5.13.2)", "lark (==1.1.9)", "mypy (==1.5)", "pre-commit", "py-evm (>=0.10.1b1,<0.11)", "pyinstaller", "pyrevm (>=0.3.2)", "pytest (>=8.0,<9.0)", "pytest-cov (>=4.1,<5.0)", "pytest-instafail (>=0.4,<1.0)", "pytest-split (>=0.7.0,<1.0)", "pytest-xdist (>=3.0,<3.4)", "setuptools", "twine"] -lint = ["black (==23.12.0)", "flake8 (==6.1.0)", "flake8-bugbear (==23.12.2)", "flake8-use-fstring (==1.4)", "isort (==5.13.2)", "mypy (==1.5)"] -test = ["eth-abi (>=5.0.0,<6.0.0)", "eth-account (==0.12.2)", "eth-stdlib (==0.2.7)", "hexbytes (>=1.2)", "hypothesis[lark] (>=6.0,<7.0)", "lark (==1.1.9)", "py-evm (>=0.10.1b1,<0.11)", "pyrevm (>=0.3.2)", "pytest (>=8.0,<9.0)", "pytest-cov (>=4.1,<5.0)", "pytest-instafail (>=0.4,<1.0)", "pytest-split (>=0.7.0,<1.0)", "pytest-xdist (>=3.0,<3.4)", "setuptools"] - -[[package]] -name = "watchdog" -version = "6.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.9" -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, - {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, - {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "wheel" -version = "0.43.0" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, - {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=65)"] - -[[package]] -name = "wmctrl" -version = "0.5" -description = "A tool to programmatically control windows inside X" -optional = false -python-versions = ">=2.7" -files = [ - {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"}, - {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"}, -] - -[package.dependencies] -attrs = "*" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "xattr" -version = "1.1.0" -description = "Python wrapper for extended filesystem attributes" -optional = false -python-versions = ">=3.8" -files = [ - {file = "xattr-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef2fa0f85458736178fd3dcfeb09c3cf423f0843313e25391db2cfd1acec8888"}, - {file = "xattr-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccab735d0632fe71f7d72e72adf886f45c18b7787430467ce0070207882cfe25"}, - {file = "xattr-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9013f290387f1ac90bccbb1926555ca9aef75651271098d99217284d9e010f7c"}, - {file = "xattr-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcd5dfbcee73c7be057676ecb900cabb46c691aff4397bf48c579ffb30bb963"}, - {file = "xattr-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6480589c1dac7785d1f851347a32c4a97305937bf7b488b857fe8b28a25de9e9"}, - {file = "xattr-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08f61cbed52dc6f7c181455826a9ff1e375ad86f67dd9d5eb7663574abb32451"}, - {file = "xattr-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:918e1f83f2e8a072da2671eac710871ee5af337e9bf8554b5ce7f20cdb113186"}, - {file = "xattr-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0f06e0c1e4d06b4e0e49aaa1184b6f0e81c3758c2e8365597918054890763b53"}, - {file = "xattr-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a641ac038a9f53d2f696716147ca4dbd6a01998dc9cd4bc628801bc0df7f4d"}, - {file = "xattr-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e4ca0956fd11679bb2e0c0d6b9cdc0f25470cc00d8da173bb7656cc9a9cf104"}, - {file = "xattr-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6881b120f9a4b36ccd8a28d933bc0f6e1de67218b6ce6e66874e0280fc006844"}, - {file = "xattr-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dab29d9288aa28e68a6f355ddfc3f0a7342b40c9012798829f3e7bd765e85c2c"}, - {file = "xattr-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0c80bbf55339c93770fc294b4b6586b5bf8e85ec00a4c2d585c33dbd84b5006"}, - {file = "xattr-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1418705f253b6b6a7224b69773842cac83fcbcd12870354b6e11dd1cd54630f"}, - {file = "xattr-1.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687e7d18611ef8d84a6ecd8f4d1ab6757500c1302f4c2046ce0aa3585e13da3f"}, - {file = "xattr-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6ceb9efe0657a982ccb8b8a2efe96b690891779584c901d2f920784e5d20ae3"}, - {file = "xattr-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b489b7916f239100956ea0b39c504f3c3a00258ba65677e4c8ba1bd0b5513446"}, - {file = "xattr-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0a9c431b0e66516a078125e9a273251d4b8e5ba84fe644b619f2725050d688a0"}, - {file = "xattr-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1a5921ea3313cc1c57f2f53b63ea8ca9a91e48f4cc7ebec057d2447ec82c7efe"}, - {file = "xattr-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6ad2a7bd5e6cf71d4a862413234a067cf158ca0ae94a40d4b87b98b62808498"}, - {file = "xattr-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0683dae7609f7280b0c89774d00b5957e6ffcb181c6019c46632b389706b77e6"}, - {file = "xattr-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54cb15cd94e5ef8a0ef02309f1bf973ba0e13c11e87686e983f371948cfee6af"}, - {file = "xattr-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff6223a854229055e803c2ad0c0ea9a6da50c6be30d92c198cf5f9f28819a921"}, - {file = "xattr-1.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d44e8f955218638c9ab222eed21e9bd9ab430d296caf2176fb37abe69a714e5c"}, - {file = "xattr-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:caab2c2986c30f92301f12e9c50415d324412e8e6a739a52a603c3e6a54b3610"}, - {file = "xattr-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d6eb7d5f281014cd44e2d847a9107491af1bf3087f5afeded75ed3e37ec87239"}, - {file = "xattr-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:47a3bdfe034b4fdb70e5941d97037405e3904accc28e10dbef6d1c9061fb6fd7"}, - {file = "xattr-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:00d2b415cf9d6a24112d019e721aa2a85652f7bbc9f3b9574b2d1cd8668eb491"}, - {file = "xattr-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:78b377832dd0ee408f9f121a354082c6346960f7b6b1480483ed0618b1912120"}, - {file = "xattr-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6461a43b585e5f2e049b39bcbfcb6391bfef3c5118231f1b15d10bdb89ef17fe"}, - {file = "xattr-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24d97f0d28f63695e3344ffdabca9fcc30c33e5c8ccc198c7524361a98d526f2"}, - {file = "xattr-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ad47d89968c9097900607457a0c89160b4771601d813e769f68263755516065"}, - {file = "xattr-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc53cab265f6e8449bd683d5ee3bc5a191e6dd940736f3de1a188e6da66b0653"}, - {file = "xattr-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cd11e917f5b89f2a0ad639d9875943806c6c9309a3dd02da5a3e8ef92db7bed9"}, - {file = "xattr-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9c5a78c7558989492c4cb7242e490ffb03482437bf782967dfff114e44242343"}, - {file = "xattr-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cebcf8a303a44fbc439b68321408af7267507c0d8643229dbb107f6c132d389c"}, - {file = "xattr-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b0d73150f2f9655b4da01c2369eb33a294b7f9d56eccb089819eafdbeb99f896"}, - {file = "xattr-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:793c01deaadac50926c0e1481702133260c7cb5e62116762f6fe1543d07b826f"}, - {file = "xattr-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e189e440bcd04ccaad0474720abee6ee64890823ec0db361fb0a4fb5e843a1bf"}, - {file = "xattr-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afacebbc1fa519f41728f8746a92da891c7755e6745164bd0d5739face318e86"}, - {file = "xattr-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b1664edf003153ac8d1911e83a0fc60db1b1b374ee8ac943f215f93754a1102"}, - {file = "xattr-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda2684228798e937a7c29b0e1c7ef3d70e2b85390a69b42a1c61b2039ba81de"}, - {file = "xattr-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b735ac2625a4fc2c9343b19f806793db6494336338537d2911c8ee4c390dda46"}, - {file = "xattr-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fa6a7af7a4ada43f15ccc58b6f9adcdbff4c36ba040013d2681e589e07ae280a"}, - {file = "xattr-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1059b2f726e2702c8bbf9bbf369acfc042202a4cc576c2dec6791234ad5e948"}, - {file = "xattr-1.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e2255f36ebf2cb2dbf772a7437ad870836b7396e60517211834cf66ce678b595"}, - {file = "xattr-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba4f80b9855cc98513ddf22b7ad8551bc448c70d3147799ea4f6c0b758fb466"}, - {file = "xattr-1.1.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb70c16e7c3ae6ba0ab6c6835c8448c61d8caf43ea63b813af1f4dbe83dd156"}, - {file = "xattr-1.1.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83652910ef6a368b77b00825ad67815e5c92bfab551a848ca66e9981d14a7519"}, - {file = "xattr-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7a92aff66c43fa3e44cbeab7cbeee66266c91178a0f595e044bf3ce51485743b"}, - {file = "xattr-1.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d4f71b673339aeaae1f6ea9ef8ea6c9643c8cd0df5003b9a0eaa75403e2e06c"}, - {file = "xattr-1.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a20de1c47b5cd7b47da61799a3b34e11e5815d716299351f82a88627a43f9a96"}, - {file = "xattr-1.1.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23705c7079b05761ff2fa778ad17396e7599c8759401abc05b312dfb3bc99f69"}, - {file = "xattr-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:27272afeba8422f2a9d27e1080a9a7b807394e88cce73db9ed8d2dde3afcfb87"}, - {file = "xattr-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd43978966de3baf4aea367c99ffa102b289d6c2ea5f3d9ce34a203dc2f2ab73"}, - {file = "xattr-1.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ded771eaf27bb4eb3c64c0d09866460ee8801d81dc21097269cf495b3cac8657"}, - {file = "xattr-1.1.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca300c0acca4f0cddd2332bb860ef58e1465d376364f0e72a1823fdd58e90d"}, - {file = "xattr-1.1.0.tar.gz", hash = "sha256:fecbf3b05043ed3487a28190dec3e4c4d879b2fcec0e30bafd8ec5d4b6043630"}, -] - -[package.dependencies] -cffi = ">=1.16.0" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.10,<4" -content-hash = "5eafe79451ba6c6b3d2b295bf09be587b1e0f328ef51de8501bb8f772ee2ed36" diff --git a/pyproject.toml b/pyproject.toml index bb3a953e..5f803b4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,45 @@ -[tool.poetry] +[project] name = "curve-stablecoin" -version = "0.2.0" -description = "" -authors = ["Michael Egorov "] +version = "2.0.0" +description = "Curve Stablecoin contracts and tests" readme = "README.md" -packages = [] -package-mode = false +requires-python = ">=3.10,<4" +authors = [ + { name = "Michael Egorov", email = "michael@curve.fi" } +] -[tool.poetry.dependencies] -python = ">=3.10,<4" -vyper = "0.4.1" -poetry = "^1" -titanoboa = "0.2.6" -hypothesis = "^6.99.0" -pdbpp = "^0.10.3" -jedi = "^0.19.0" -pytest-xdist = "^3.5" -pytest-forked = "^1.6.0" -ipython = "^8" -cytoolz = "^0.12" -snekmate = "^0.1.1" +dependencies = [ + "vyper==0.4.3", + "snekmate>=0.1.1", + "ownership-proxy", + "curve-std", +] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[dependency-groups] +dev = [ + "titanoboa", + "hypothesis>=6.99.0", + "pytest>=8.0.0", + "pytest-xdist>=3.5", + "pytest-forked>=1.6.0", + "pytest-cov>=4.0.0", + "pytest-profiling>=1.8.1", + "pre-commit==4.3.0", + "z3-solver>=4.12.0", +] + +[tool.uv.sources] +titanoboa = { git = "https://github.com/AlbertoCentonze/titanoboa", rev = "vvm-eval" } +# TODO fix a version +ownership-proxy = { git = "https://github.com/curvefi/ownership-proxy", rev = "main" } +# TODO fix a version +curve-std = { git = "https://github.com/curvefi/curve-std", rev = "main" } +# TODO add stableswap here as a package instead of git submodule + + +[tool.ruff.lint] +ignore = [ + # Should be eventually fixed + "E722" , + "F821" +] diff --git a/pypytest b/pypytest deleted file mode 100755 index 9993a386..00000000 --- a/pypytest +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# pypy3 -m pytest $@ --durations=0 -vv --disable-pytest-warnings -pypy3 --jit enable_opts=all,threshold=50,vec=1,vec_all=1 -m pytest $@ --durations=0 -vv --disable-pytest-warnings --ignore=tests/stablecoin diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a57a2509..00000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = --disable-pytest-warnings --ignore=tests/stablecoin diff --git a/scripts/ape-deploy-amm-controller-Aug17.py b/scripts/ape-deploy-amm-controller-Aug17.py index ce32eca5..46c8722c 100644 --- a/scripts/ape-deploy-amm-controller-Aug17.py +++ b/scripts/ape-deploy-amm-controller-Aug17.py @@ -4,6 +4,7 @@ from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -19,15 +20,15 @@ def deploy_blueprint(contract, account, just_bytecode=False, **kw): if just_bytecode: return initcode initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -46,12 +47,12 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): controller_impl = deploy_blueprint(project.Controller, account, **kw) @@ -59,8 +60,8 @@ def deploy(network): print() - print('Controller implementation:', controller_impl) - print('AMM implementation:', amm_impl) + print("Controller implementation:", controller_impl) + print("AMM implementation:", amm_impl) @cli.command( @@ -70,6 +71,8 @@ def deploy(network): def verify(network): controller_bytes = api.Address(DEPLOYED_CONTROLLER).code amm_bytes = api.Address(DEPLOYED_AMM).code - assert controller_bytes == deploy_blueprint(project.Controller, None, just_bytecode=True) + assert controller_bytes == deploy_blueprint( + project.Controller, None, just_bytecode=True + ) assert amm_bytes == deploy_blueprint(project.AMM, None, just_bytecode=True) - print('Blueprints match the ones deployed on chain') + print("Blueprints match the ones deployed on chain") diff --git a/scripts/ape-deploy-amm-controller.py b/scripts/ape-deploy-amm-controller.py index 9d330fc5..4cf0460d 100644 --- a/scripts/ape-deploy-amm-controller.py +++ b/scripts/ape-deploy-amm-controller.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks, api from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -19,15 +21,15 @@ def deploy_blueprint(contract, account, just_bytecode=False, **kw): if just_bytecode: return initcode initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -46,12 +48,12 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): controller_impl = deploy_blueprint(project.Controller, account, **kw) @@ -59,8 +61,8 @@ def deploy(network): print() - print('Controller implementation:', controller_impl) - print('AMM implementation:', amm_impl) + print("Controller implementation:", controller_impl) + print("AMM implementation:", amm_impl) @cli.command( @@ -68,8 +70,10 @@ def deploy(network): ) @network_option() def verify(network): - controller_bytes = api.Address('0x9DFbf2b2aF574cA8Ba6dD3fD397287944269f720').code - amm_bytes = api.Address('0x23208cA4F2B30d8f7D54bf2D5A822D1a2F876501').code - assert controller_bytes == deploy_blueprint(project.Controller, None, just_bytecode=True) + controller_bytes = api.Address("0x9DFbf2b2aF574cA8Ba6dD3fD397287944269f720").code + amm_bytes = api.Address("0x23208cA4F2B30d8f7D54bf2D5A822D1a2F876501").code + assert controller_bytes == deploy_blueprint( + project.Controller, None, just_bytecode=True + ) assert amm_bytes == deploy_blueprint(project.AMM, None, just_bytecode=True) - print('Blueprints match the ones deployed on chain') + print("Blueprints match the ones deployed on chain") diff --git a/scripts/ape-deploy-controller-17jun.py b/scripts/ape-deploy-controller-17jun.py index ac71fdf9..4991bfd6 100644 --- a/scripts/ape-deploy-controller-17jun.py +++ b/scripts/ape-deploy-controller-17jun.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks, api from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -19,15 +21,15 @@ def deploy_blueprint(contract, account, just_bytecode=False, **kw): if just_bytecode: return initcode initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -46,19 +48,19 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): controller_impl = deploy_blueprint(project.Controller, account, **kw) print() - print('Controller implementation:', controller_impl) + print("Controller implementation:", controller_impl) @cli.command( @@ -66,6 +68,8 @@ def deploy(network): ) @network_option() def verify(network): - controller_bytes = api.Address('0xCdb55051fC792303DdC7c1052cC5161BaeD88e2A').code - assert controller_bytes == deploy_blueprint(project.Controller, None, just_bytecode=True) - print('Blueprints match the ones deployed on chain') + controller_bytes = api.Address("0xCdb55051fC792303DdC7c1052cC5161BaeD88e2A").code + assert controller_bytes == deploy_blueprint( + project.Controller, None, just_bytecode=True + ) + print("Blueprints match the ones deployed on chain") diff --git a/scripts/ape-deploy-deleverage-zaps.py b/scripts/ape-deploy-deleverage-zaps.py index a5b3b434..5ecd9c7c 100644 --- a/scripts/ape-deploy-deleverage-zaps.py +++ b/scripts/ape-deploy-deleverage-zaps.py @@ -1,5 +1,6 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click @@ -39,95 +40,119 @@ "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "tricrv": { "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], ], }, @@ -135,23 +160,29 @@ "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], ], }, @@ -161,95 +192,119 @@ "name": "wstETH wrapper -> steth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "wstETH wrapper -> steth -> tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "tricrv": { "name": "wstETH wrapper -> steth -> factory-tricrypto-4 (TriCRV)", "route": [ COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "wstETH wrapper -> steth -> tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], ], }, @@ -257,23 +312,29 @@ "name": "wstETH wrapper -> steth -> tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], ], }, @@ -283,120 +344,150 @@ "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["WBTC"], - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { "name": "tricrypto2 -> factory-v2-59 (USDP) -> crvUSD/USDP", "route": [ COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", CRVUSD_POOLS["USDP"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", CRVUSD_POOLS["USDP"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { "name": "tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -405,120 +496,150 @@ "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["WETH"], - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tricrv": { "name": "factory-tricrypto-4 (TriCRV)", "route": [ COLLATERALS["WETH"], - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { "name": "tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -527,95 +648,119 @@ "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "tricrv": { "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], ], }, @@ -623,23 +768,29 @@ "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], ], }, @@ -649,123 +800,153 @@ "name": "factory-tricrypto-2 (TricryptoLLAMA)", "route": [ COLLATERALS["tBTC"], - '0x2889302a794da87fbf1d6db415c1492194663d13', + "0x2889302a794da87fbf1d6db415c1492194663d13", CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0x2889302a794da87fbf1d6db415c1492194663d13', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x2889302a794da87fbf1d6db415c1492194663d13", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdc": { "name": "factory-crvusd-16 (tBTC/WBTC) -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", "route": [ COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", CRVUSD_POOLS["USDC"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> crvUSD/USDT", "route": [ COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", CRVUSD_POOLS["USDT"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> tusd -> crvUSD/TUSD", "route": [ COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", CRVUSD_POOLS["TUSD"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, "frax": { "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> frax -> crvUSD/FRAX", "route": [ COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", CRVUSD_POOLS["FRAX"], CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", ], }, - } + }, } @@ -784,19 +965,20 @@ def deploy(network): kw = {} # Deployer address - if ':local:' in network: + if ":local:" in network: account = accounts.test_accounts[0] - elif ':mainnet:' in network: - account = accounts.load('babe') + elif ":mainnet:" in network: + account = accounts.load("babe") account.set_autosign(True) max_base_fee = networks.active_provider.base_fee * 2 kw = { - 'max_fee': max_base_fee, - 'max_priority_fee': min(int(0.5e9), max_base_fee)} + "max_fee": max_base_fee, + "max_priority_fee": min(int(0.5e9), max_base_fee), + } else: account = "0xbabe61887f1de2713c6f97e567623453d3C79f67" if account in accounts: - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) else: account = accounts.test_accounts[0] @@ -825,13 +1007,14 @@ def deploy(network): **kw, ) - print('========================') - print('sfrxETH: ', deleverage_contracts["sfrxETH"].address) - print('wstETH: ', deleverage_contracts["wstETH"].address) - print('WBTC: ', deleverage_contracts["WBTC"].address) - print('WETH: ', deleverage_contracts["WETH"].address) - print('sfrxETH2: ', deleverage_contracts["sfrxETH2"].address) - print('tBTC: ', deleverage_contracts["tBTC"].address) + print("========================") + print("sfrxETH: ", deleverage_contracts["sfrxETH"].address) + print("wstETH: ", deleverage_contracts["wstETH"].address) + print("WBTC: ", deleverage_contracts["WBTC"].address) + print("WETH: ", deleverage_contracts["WETH"].address) + print("sfrxETH2: ", deleverage_contracts["sfrxETH2"].address) + print("tBTC: ", deleverage_contracts["tBTC"].address) import IPython + IPython.embed() diff --git a/scripts/ape-deploy-leverage-zaps.py b/scripts/ape-deploy-leverage-zaps.py index c5784927..bb9d8b70 100644 --- a/scripts/ape-deploy-leverage-zaps.py +++ b/scripts/ape-deploy-leverage-zaps.py @@ -1,7 +1,6 @@ -from time import sleep -from ape import project, accounts, Contract, networks +from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option -from ape import chain + # account_option could be used when in prod? import click @@ -53,20 +52,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", frxETH, ], "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -74,20 +73,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", frxETH, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { @@ -95,20 +94,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", frxETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -116,20 +115,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", frxETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -137,20 +136,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", frxETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -160,20 +159,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", stETH, ], "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -181,20 +180,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", stETH, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { @@ -202,20 +201,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", stETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -223,20 +222,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", stETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -244,20 +243,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", stETH, ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -267,20 +266,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [1, 2, 1], [0, 1, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -288,20 +287,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 1, 3], [0, 0, 0], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { @@ -309,20 +308,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -330,20 +329,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -351,20 +350,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -374,20 +373,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -395,20 +394,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 2, 3], [0, 0, 0], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { @@ -416,20 +415,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -437,20 +436,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -458,20 +457,20 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -481,23 +480,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", COLLATERALS["sfrxETH"], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 2, 1, 1, 3], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 2, 1, 1, 3], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], "factory_swap_addresses": [ CRVUSD_POOLS["USDC"], - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -505,23 +510,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", COLLATERALS["sfrxETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ CRVUSD_POOLS["USDT"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdp": { @@ -529,23 +540,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", COLLATERALS["sfrxETH"], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], "factory_swap_addresses": [ CRVUSD_POOLS["USDP"], - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -553,23 +570,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", COLLATERALS["sfrxETH"], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], "factory_swap_addresses": [ CRVUSD_POOLS["TUSD"], - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -577,23 +600,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", COLLATERALS["sfrxETH"], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], "factory_swap_addresses": [ CRVUSD_POOLS["FRAX"], - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -602,24 +631,30 @@ "name": "factory-tricrypto-2 (TricryptoLLAMA)", "route": [ CRVUSD, - '0x2889302a794da87fbf1d6db415c1492194663d13', + "0x2889302a794da87fbf1d6db415c1492194663d13", COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 1, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 1, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], "factory_swap_addresses": [ - '0x2889302a794da87fbf1d6db415c1492194663d13', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x2889302a794da87fbf1d6db415c1492194663d13", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "usdc": { @@ -627,23 +662,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 2, 1, 1, 3], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [1, 2, 1, 1, 3], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ CRVUSD_POOLS["USDC"], - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", ], }, "usdt": { @@ -651,23 +692,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ CRVUSD_POOLS["USDT"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", ], }, "tusd": { @@ -675,23 +722,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ CRVUSD_POOLS["TUSD"], - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", ], }, "frax": { @@ -699,23 +752,29 @@ "route": [ CRVUSD, CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], "factory_swap_addresses": [ CRVUSD_POOLS["FRAX"], - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", ], }, }, @@ -737,19 +796,20 @@ def deploy(network): kw = {} # Deployer address - if ':local:' in network: + if ":local:" in network: account = accounts.test_accounts[0] - elif ':mainnet:' in network: - account = accounts.load('babe') + elif ":mainnet:" in network: + account = accounts.load("babe") account.set_autosign(True) max_base_fee = networks.active_provider.base_fee * 2 kw = { - 'max_fee': max_base_fee, - 'max_priority_fee': min(int(0.5e9), max_base_fee)} + "max_fee": max_base_fee, + "max_priority_fee": min(int(0.5e9), max_base_fee), + } else: account = "0xbabe61887f1de2713c6f97e567623453d3C79f67" if account in accounts: - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) else: account = accounts.test_accounts[0] @@ -786,15 +846,14 @@ def deploy(network): **kw, ) - - print('========================') + print("========================") # print('sfrxETH: ', leverage_contracts["sfrxETH"].address) # print('wstETH: ', leverage_contracts["wstETH"].address) # print('WBTC: ', leverage_contracts["WBTC"].address) # print('WETH: ', leverage_contracts["WETH"].address) - print('sfrxETH2: ', leverage_contracts["sfrxETH2"].address) - print('tBTC: ', leverage_contracts["tBTC"].address) - + print("sfrxETH2: ", leverage_contracts["sfrxETH2"].address) + print("tBTC: ", leverage_contracts["tBTC"].address) import IPython + IPython.embed() diff --git a/scripts/ape-deploy-owner-proxy.py b/scripts/ape-deploy-owner-proxy.py index ffe996e1..3f9b2dae 100644 --- a/scripts/ape-deploy-owner-proxy.py +++ b/scripts/ape-deploy-owner-proxy.py @@ -3,11 +3,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -31,16 +33,22 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): - owner_proxy = account.deploy(project.OwnerProxy, - OWNERSHIP_ADMIN, PARAMETER_ADMIN, EMERGENCY_ADMIN, - SWAP_FACTORY, ZERO_ADDRESS, **kw) - - print('OwnerProxy:', owner_proxy) + owner_proxy = account.deploy( + project.OwnerProxy, + OWNERSHIP_ADMIN, + PARAMETER_ADMIN, + EMERGENCY_ADMIN, + SWAP_FACTORY, + ZERO_ADDRESS, + **kw, + ) + + print("OwnerProxy:", owner_proxy) diff --git a/scripts/ape-deploy-sepolia.py b/scripts/ape-deploy-sepolia.py index 690114ad..0df4f071 100644 --- a/scripts/ape-deploy-sepolia.py +++ b/scripts/ape-deploy-sepolia.py @@ -1,10 +1,12 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env-testnet")) @@ -63,15 +65,15 @@ def deploy_blueprint(contract, account, **kw): initcode = bytes.fromhex(initcode.removeprefix("0x")) initcode = b"\xfe\x71\x00" + initcode # eip-5202 preamble version 0 initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -92,20 +94,18 @@ def cli(): def deploy(network): kw = {} - assert 'sepolia' in network, "This script is ONLY to test on Sepolia" + assert "sepolia" in network, "This script is ONLY to test on Sepolia" # Deployer address - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_base_fee = networks.active_provider.base_fee * 2 - kw = { - 'max_fee': max_base_fee, - 'max_priority_fee': min(int(0.5e9), max_base_fee)} + kw = {"max_fee": max_base_fee, "max_priority_fee": min(int(0.5e9), max_base_fee)} temporary_admin = account # Admin and fee receiver <- DAO if in prod or mainnet-fork - if 'mainnet' in network: + if "mainnet" in network: admin = OWNERSHIP_ADMIN # Ownership admin fee_receiver = FEE_RECEIVER # 0xECB for fee collection else: @@ -121,7 +121,14 @@ def deploy(network): print("Deploying stablecoin") stablecoin = account.deploy(project.Stablecoin, FULL_NAME, SHORT_NAME, **kw) print("Deploying factory") - factory = account.deploy(project.ControllerFactory, stablecoin, temporary_admin, weth, fee_receiver, **kw) + factory = account.deploy( + project.ControllerFactory, + stablecoin, + temporary_admin, + weth, + fee_receiver, + **kw, + ) print("Deploying Controller and AMM implementations") controller_impl = deploy_blueprint(project.Controller, account, **kw) @@ -136,15 +143,23 @@ def deploy(network): swap_factory.add_token_to_whitelist(stablecoin, True, **kw) # Ownership admin is account temporarily, will need to become OWNERSHIP_ADMIN - owner_proxy = account.deploy(project.OwnerProxy, - temporary_admin, PARAMETER_ADMIN, EMERGENCY_ADMIN, - swap_factory, ZERO_ADDRESS, **kw) + owner_proxy = account.deploy( + project.OwnerProxy, + temporary_admin, + PARAMETER_ADMIN, + EMERGENCY_ADMIN, + swap_factory, + ZERO_ADDRESS, + **kw, + ) swap_factory.commit_transfer_ownership(owner_proxy, **kw) owner_proxy.accept_transfer_ownership(swap_factory, **kw) # Set implementations stableswap_impl = account.deploy(project.Stableswap, **kw) - owner_proxy.set_plain_implementations(swap_factory, 2, [stableswap_impl.address] + [ZERO_ADDRESS] * 9, **kw) + owner_proxy.set_plain_implementations( + swap_factory, 2, [stableswap_impl.address] + [ZERO_ADDRESS] * 9, **kw + ) owner_proxy.set_gauge_implementation(swap_factory, GAUGE_IMPL, **kw) pools = {} @@ -161,16 +176,21 @@ def deploy(network): stable_asset_type, 0, # implentation_idx stable_ma_exp_time, - **kw) + **kw, + ) # This is a workaround: instead of getting return_value we parse events to get the pool address # This is because reading return_value in ape is broken - pool = project.Stableswap.at(tx.events.filter(swap_factory.PlainPoolDeployed)[0].pool) + pool = project.Stableswap.at( + tx.events.filter(swap_factory.PlainPoolDeployed)[0].pool + ) print(f"Stablecoin pool crvUSD/{name} is deployed at {pool.address}") pools[name] = pool # Price aggregator print("Deploying stable price aggregator") - agg = account.deploy(project.AggregateStablePrice, stablecoin, 10**15, temporary_admin, **kw) + agg = account.deploy( + project.AggregateStablePrice, stablecoin, 10**15, temporary_admin, **kw + ) for pool in pools.values(): agg.add_price_pair(pool, **kw) agg.set_admin(admin, **kw) # Alternatively, we can make it ZERO_ADDRESS @@ -179,15 +199,31 @@ def deploy(network): peg_keepers = [] for pool in pools.values(): print(f"Deploying a PegKeeper for {pool.name()}") - peg_keeper = account.deploy(project.PegKeeper, pool, 1, FEE_RECEIVER, 2 * 10**4, factory, agg, - admin, **kw) + peg_keeper = account.deploy( + project.PegKeeper, + pool, + 1, + FEE_RECEIVER, + 2 * 10**4, + factory, + agg, + admin, + **kw, + ) peg_keepers.append(peg_keeper) factory.set_debt_ceiling(peg_keeper, initial_pk_funds, **kw) - policy = account.deploy(project.AggMonetaryPolicy, admin, agg, factory, - peg_keepers + [ZERO_ADDRESS] * 2, - policy_rate, policy_sigma, policy_debt_fraction, - **kw) + policy = account.deploy( + project.AggMonetaryPolicy, + admin, + agg, + factory, + peg_keepers + [ZERO_ADDRESS] * 2, + policy_rate, + policy_sigma, + policy_debt_fraction, + **kw, + ) # Pure ETH # price_oracle = account.deploy( @@ -200,44 +236,54 @@ def deploy(network): # oracle_ema, # oracle_bound_size) - tricrypto = account.deploy(project.TricryptoMock, [rtokens['USDT'], weth, weth], **kw) + tricrypto = account.deploy( + project.TricryptoMock, [rtokens["USDT"], weth, weth], **kw + ) # sFrxETH price_oracle = account.deploy( - project.CryptoWithStablePriceAndChainlinkFrxeth, - tricrypto, # Tricrypto - 1, # price index with ETH - pools["USDT"], - FRXETH_POOL, # staked swap (frxeth/eth) - agg, - weth, # ETH/USD Chainlink aggregator - we use any ERC20 here to make the init work - SFRXETH, # sFrxETH - 600, - 1, # 1% bound - **kw) + project.CryptoWithStablePriceAndChainlinkFrxeth, + tricrypto, # Tricrypto + 1, # price index with ETH + pools["USDT"], + FRXETH_POOL, # staked swap (frxeth/eth) + agg, + weth, # ETH/USD Chainlink aggregator - we use any ERC20 here to make the init work + SFRXETH, # sFrxETH + 600, + 1, # 1% bound + **kw, + ) - debug_price_oracle = account.deploy(project.DummyPriceOracle, temporary_admin, 3000 * 10**18) + debug_price_oracle = account.deploy( + project.DummyPriceOracle, temporary_admin, 3000 * 10**18 + ) factory.add_market( - collateral, market_A, market_fee, market_admin_fee, - debug_price_oracle, policy, - market_loan_discount, market_liquidation_discount, + collateral, + market_A, + market_fee, + market_admin_fee, + debug_price_oracle, + policy, + market_loan_discount, + market_liquidation_discount, market_debt_ceiling, - **kw + **kw, ) amm = project.AMM.at(factory.get_amm(collateral)) controller = project.Controller.at(factory.get_controller(collateral)) - print('========================') - print('Stablecoin: ', stablecoin.address) - print('Factory: ', factory.address) - print('Collateral: ', collateral) - print('AMM: ', amm.address) - print('Controller: ', controller.address) - print('Price Oracle: ', price_oracle.address) - print('Monetary policy: ', policy.address) - print('Swap factory: ', swap_factory.address) - print('Owner proxy: ', owner_proxy.address) - print('Price Aggregator: ', agg.address) - print('PegKeepers: ', [pk.address for pk in peg_keepers]) + print("========================") + print("Stablecoin: ", stablecoin.address) + print("Factory: ", factory.address) + print("Collateral: ", collateral) + print("AMM: ", amm.address) + print("Controller: ", controller.address) + print("Price Oracle: ", price_oracle.address) + print("Monetary policy: ", policy.address) + print("Swap factory: ", swap_factory.address) + print("Owner proxy: ", owner_proxy.address) + print("Price Aggregator: ", agg.address) + print("PegKeepers: ", [pk.address for pk in peg_keepers]) diff --git a/scripts/ape-deploy.py b/scripts/ape-deploy.py index a94072fe..fc39e55f 100644 --- a/scripts/ape-deploy.py +++ b/scripts/ape-deploy.py @@ -1,10 +1,12 @@ from ape import project, accounts, Contract, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -46,7 +48,9 @@ stable_asset_type = 0 stable_ma_exp_time = 866 # 10 min / ln(2) -policy_rate = int((1.1**(1 / (365 * 86400)) - 1) * 1e18) # 10% if PegKeepers are empty, 4% when at target fraction +policy_rate = int( + (1.1 ** (1 / (365 * 86400)) - 1) * 1e18 +) # 10% if PegKeepers are empty, 4% when at target fraction policy_sigma = 2 * 10**16 # 2% when at target debt fraction policy_debt_fraction = 10 * 10**16 # 10% @@ -68,15 +72,15 @@ def deploy_blueprint(contract, account, **kw): initcode = bytes.fromhex(initcode.removeprefix("0x")) initcode = b"\xfe\x71\x00" + initcode # eip-5202 preamble version 0 initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -98,25 +102,26 @@ def deploy(network): kw = {} # Deployer address - if ':local:' in network: + if ":local:" in network: account = accounts.test_accounts[0] - elif ':mainnet:' in network: - account = accounts.load('babe') + elif ":mainnet:" in network: + account = accounts.load("babe") account.set_autosign(True) max_base_fee = networks.active_provider.base_fee * 2 kw = { - 'max_fee': max_base_fee, - 'max_priority_fee': min(int(0.5e9), max_base_fee)} + "max_fee": max_base_fee, + "max_priority_fee": min(int(0.5e9), max_base_fee), + } else: account = "0xbabe61887f1de2713c6f97e567623453d3C79f67" if account in accounts: - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) temporary_admin = account # Admin and fee receiver <- DAO if in prod or mainnet-fork - if 'mainnet' in network: + if "mainnet" in network: admin = OWNERSHIP_ADMIN # Ownership admin fee_receiver = FEE_RECEIVER # 0xECB for fee collection else: @@ -125,7 +130,7 @@ def deploy(network): with accounts.use_sender(account) as account: # Real or fake wETH - if 'mainnet' in network: + if "mainnet" in network: weth = Contract(WETH) collateral = SFRXETH else: @@ -136,7 +141,14 @@ def deploy(network): print("Deploying stablecoin") stablecoin = account.deploy(project.Stablecoin, FULL_NAME, SHORT_NAME, **kw) print("Deploying factory") - factory = account.deploy(project.ControllerFactory, stablecoin, temporary_admin, fee_receiver, weth, **kw) + factory = account.deploy( + project.ControllerFactory, + stablecoin, + temporary_admin, + fee_receiver, + weth, + **kw, + ) print("Deploying Controller and AMM implementations") controller_impl = deploy_blueprint(project.Controller, account, **kw) @@ -147,24 +159,34 @@ def deploy(network): stablecoin.set_minter(factory, **kw) # Deploy plain pools and stabilizer - if 'mainnet' in network: + if "mainnet" in network: swap_factory = account.deploy(project.StableswapFactory, FEE_RECEIVER, **kw) swap_factory.add_token_to_whitelist(stablecoin, True, **kw) # Ownership admin is account temporarily, will need to become OWNERSHIP_ADMIN - owner_proxy = account.deploy(project.OwnerProxy, - temporary_admin, PARAMETER_ADMIN, EMERGENCY_ADMIN, - swap_factory, ZERO_ADDRESS, **kw) + owner_proxy = account.deploy( + project.OwnerProxy, + temporary_admin, + PARAMETER_ADMIN, + EMERGENCY_ADMIN, + swap_factory, + ZERO_ADDRESS, + **kw, + ) swap_factory.commit_transfer_ownership(owner_proxy, **kw) owner_proxy.accept_transfer_ownership(swap_factory, **kw) # Set implementations stableswap_impl = account.deploy(project.Stableswap, **kw) - owner_proxy.set_plain_implementations(swap_factory, 2, [stableswap_impl.address] + [ZERO_ADDRESS] * 9, **kw) + owner_proxy.set_plain_implementations( + swap_factory, 2, [stableswap_impl.address] + [ZERO_ADDRESS] * 9, **kw + ) owner_proxy.set_gauge_implementation(swap_factory, GAUGE_IMPL, **kw) # Set all admins to the DAO - owner_proxy.commit_set_admins(OWNERSHIP_ADMIN, PARAMETER_ADMIN, EMERGENCY_ADMIN, **kw) + owner_proxy.commit_set_admins( + OWNERSHIP_ADMIN, PARAMETER_ADMIN, EMERGENCY_ADMIN, **kw + ) owner_proxy.apply_set_admins(**kw) if SET_ADDRESS_PROVIDER: @@ -172,20 +194,23 @@ def deploy(network): address_provider = Contract(ADDRESS_PROVIDER) address_provider_admin = Contract(address_provider.admin()) - if address_provider.get_address(STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) == ZERO_ADDRESS: + if ( + address_provider.get_address(STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) + == ZERO_ADDRESS + ): address_provider_admin.execute( - address_provider, - address_provider.add_new_id.encode_input(swap_factory, 'crvUSD plain pools'), - **kw + address_provider, + address_provider.add_new_id.encode_input( + swap_factory, "crvUSD plain pools" + ), + **kw, ) else: address_provider_admin.execute( address_provider, address_provider.set_address.encode_input( - STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID, - swap_factory, - **kw + STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID, swap_factory, **kw ), ) @@ -203,16 +228,21 @@ def deploy(network): stable_asset_type, 0, # implentation_idx stable_ma_exp_time, - **kw) + **kw, + ) # This is a workaround: instead of getting return_value we parse events to get the pool address # This is because reading return_value in ape is broken - pool = project.Stableswap.at(tx.events.filter(swap_factory.PlainPoolDeployed)[0].pool) + pool = project.Stableswap.at( + tx.events.filter(swap_factory.PlainPoolDeployed)[0].pool + ) print(f"Stablecoin pool crvUSD/{name} is deployed at {pool.address}") pools[name] = pool # Price aggregator print("Deploying stable price aggregator") - agg = account.deploy(project.AggregateStablePrice, stablecoin, 10**15, temporary_admin, **kw) + agg = account.deploy( + project.AggregateStablePrice, stablecoin, 10**15, temporary_admin, **kw + ) for pool in pools.values(): agg.add_price_pair(pool, **kw) agg.set_admin(admin, **kw) # Alternatively, we can make it ZERO_ADDRESS @@ -221,16 +251,32 @@ def deploy(network): peg_keepers = [] for pool in pools.values(): print(f"Deploying a PegKeeper for {pool.name()}") - peg_keeper = account.deploy(project.PegKeeper, pool, 1, FEE_RECEIVER, 2 * 10**4, factory, agg, - admin, **kw) + peg_keeper = account.deploy( + project.PegKeeper, + pool, + 1, + FEE_RECEIVER, + 2 * 10**4, + factory, + agg, + admin, + **kw, + ) peg_keepers.append(peg_keeper) factory.set_debt_ceiling(peg_keeper, initial_pk_funds, **kw) - if 'mainnet' in network: - policy = account.deploy(project.AggMonetaryPolicy, admin, agg, factory, - peg_keepers + [ZERO_ADDRESS], - policy_rate, policy_sigma, policy_debt_fraction, - **kw) + if "mainnet" in network: + policy = account.deploy( + project.AggMonetaryPolicy, + admin, + agg, + factory, + peg_keepers + [ZERO_ADDRESS], + policy_rate, + policy_sigma, + policy_debt_fraction, + **kw, + ) # Pure ETH # price_oracle = account.deploy( @@ -245,34 +291,42 @@ def deploy(network): # sFrxETH price_oracle = account.deploy( - project.CryptoWithStablePriceAndChainlinkFrxeth, - TRICRYPTO, # Tricrypto - 1, # price index with ETH - pools["USDT"], - FRXETH_POOL, # staked swap (frxeth/eth) - agg, - CHAINLINK_ETH, # ETH/USD Chainlink aggregator - SFRXETH, # sFrxETH - 600, - 1, # 1% bound - **kw) + project.CryptoWithStablePriceAndChainlinkFrxeth, + TRICRYPTO, # Tricrypto + 1, # price index with ETH + pools["USDT"], + FRXETH_POOL, # staked swap (frxeth/eth) + agg, + CHAINLINK_ETH, # ETH/USD Chainlink aggregator + SFRXETH, # sFrxETH + 600, + 1, # 1% bound + **kw, + ) price_oracle.price_w(**kw) - print('Price oracle price: {:.2f}'.format(price_oracle.price() / 1e18)) + print("Price oracle price: {:.2f}".format(price_oracle.price() / 1e18)) else: policy = account.deploy(project.ConstantMonetaryPolicy, temporary_admin) policy.set_rate(0) # 0% policy.set_admin(admin) - price_oracle = account.deploy(project.DummyPriceOracle, admin, 3000 * 10**18) + price_oracle = account.deploy( + project.DummyPriceOracle, admin, 3000 * 10**18 + ) factory.add_market( - collateral, market_A, market_fee, market_admin_fee, - price_oracle, policy, - market_loan_discount, market_liquidation_discount, + collateral, + market_A, + market_fee, + market_admin_fee, + price_oracle, + policy, + market_loan_discount, + market_liquidation_discount, market_debt_ceiling, - **kw + **kw, ) if admin != temporary_admin: @@ -281,17 +335,20 @@ def deploy(network): amm = project.AMM.at(factory.get_amm(collateral)) controller = project.Controller.at(factory.get_controller(collateral)) - print('========================') - print('Stablecoin: ', stablecoin.address) - print('Factory: ', factory.address) - print('Collateral: ', collateral) - print('AMM: ', amm.address) - print('Controller: ', controller.address) - print('Price Oracle: ', price_oracle.address) - print('Monetary policy: ', policy.address) - if 'mainnet' in network: - print('Swap factory: ', swap_factory.address) - print('Owner proxy: ', owner_proxy.address) - print('Price Aggregator: ', agg.address) - print('PegKeepers: ', [pk.address for pk in peg_keepers]) - print('Stablecoin pools: ', ['%s:%s' % (name, pool.address) for name, pool in pools.items()]) + print("========================") + print("Stablecoin: ", stablecoin.address) + print("Factory: ", factory.address) + print("Collateral: ", collateral) + print("AMM: ", amm.address) + print("Controller: ", controller.address) + print("Price Oracle: ", price_oracle.address) + print("Monetary policy: ", policy.address) + if "mainnet" in network: + print("Swap factory: ", swap_factory.address) + print("Owner proxy: ", owner_proxy.address) + print("Price Aggregator: ", agg.address) + print("PegKeepers: ", [pk.address for pk in peg_keepers]) + print( + "Stablecoin pools: ", + ["%s:%s" % (name, pool.address) for name, pool in pools.items()], + ) diff --git a/scripts/ape-eth-oracle.py b/scripts/ape-eth-oracle.py index 72fa2d43..fca9e09f 100644 --- a/scripts/ape-eth-oracle.py +++ b/scripts/ape-eth-oracle.py @@ -1,10 +1,12 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -12,8 +14,14 @@ STABLECOIN = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" OWNERSHIP_ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" -TRICRYPTO = ["0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4"] # USDC, USDT -CRVUSD_POOLS = ["0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", "0x390f3595bca2df7d23783dfd126427cceb997bf4"] # USDC, USDT +TRICRYPTO = [ + "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", + "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4", +] # USDC, USDT +CRVUSD_POOLS = [ + "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "0x390f3595bca2df7d23783dfd126427cceb997bf4", +] # USDC, USDT FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" CHAINLINK_ETH = "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419" BOUND_SIZE = int(0.015 * 1e18) @@ -33,27 +41,28 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): oracle = account.deploy( - project.CryptoWithStablePriceETH, - TRICRYPTO, - [1, 1], # price index with ETH - CRVUSD_POOLS, # CRVUSD stableswaps - AGG, - FACTORY, - CHAINLINK_ETH, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceETH, + TRICRYPTO, + [1, 1], # price index with ETH + CRVUSD_POOLS, # CRVUSD stableswaps + AGG, + FACTORY, + CHAINLINK_ETH, + BOUND_SIZE, + **kw, + ) oracle.price_w(**kw) - print('========================') - print('Price Oracle: ', oracle.address) - print('Price: ', oracle.price() / 1e18) + print("========================") + print("Price Oracle: ", oracle.address) + print("Price: ", oracle.price() / 1e18) diff --git a/scripts/ape-frxeth-oracle.py b/scripts/ape-frxeth-oracle.py index 01813621..697c329b 100644 --- a/scripts/ape-frxeth-oracle.py +++ b/scripts/ape-frxeth-oracle.py @@ -3,19 +3,27 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) STABLECOIN = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" -TRICRYPTO = ["0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4"] # USDC, USDT -CRVUSD_POOLS = ["0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", "0x390f3595bca2df7d23783dfd126427cceb997bf4"] # USDC, USDT +TRICRYPTO = [ + "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", + "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4", +] # USDC, USDT +CRVUSD_POOLS = [ + "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "0x390f3595bca2df7d23783dfd126427cceb997bf4", +] # USDC, USDT FRXETH_POOL = "0x9c3b46c0ceb5b9e304fcd6d88fc50f7dd24b31bc" SFRXETH = "0xac3E018457B222d93114458476f3E3416Abbe38F" FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" @@ -37,28 +45,29 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): oracle = account.deploy( - project.CryptoWithStablePriceFrxethN, - TRICRYPTO, - [1, 1], # price index with ETH - CRVUSD_POOLS, # CRVUSD stableswaps - FRXETH_POOL, # staked swap (frxeth/weth) - AGG, - FACTORY, - SFRXETH, - CHAINLINK_ETH, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceFrxethN, + TRICRYPTO, + [1, 1], # price index with ETH + CRVUSD_POOLS, # CRVUSD stableswaps + FRXETH_POOL, # staked swap (frxeth/weth) + AGG, + FACTORY, + SFRXETH, + CHAINLINK_ETH, + BOUND_SIZE, + **kw, + ) oracle.price_w(**kw) - print('========================') - print('Price Oracle: ', oracle.address) + print("========================") + print("Price Oracle: ", oracle.address) diff --git a/scripts/ape-monetary-policy-2.py b/scripts/ape-monetary-policy-2.py index f29f9142..52a7788a 100644 --- a/scripts/ape-monetary-policy-2.py +++ b/scripts/ape-monetary-policy-2.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -15,12 +17,15 @@ PRICE_ORACLE = "0xe5Afcf332a5457E8FafCD668BcE3dF953762Dfe7" # Agg CONTROLLER_FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" PEG_KEEPERS = [ - '0xaA346781dDD7009caa644A4980f044C50cD2ae22', - '0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8', - '0x6B765d07cf966c745B340AdCa67749fE75B5c345', - '0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae', - '0x0000000000000000000000000000000000000000'] -RATE = int((1.1**(1 / (365 * 86400)) - 1) * 1e18) # 10% if PegKeepers are empty, 4% when at target fraction + "0xaA346781dDD7009caa644A4980f044C50cD2ae22", + "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", + "0x6B765d07cf966c745B340AdCa67749fE75B5c345", + "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", + "0x0000000000000000000000000000000000000000", +] +RATE = int( + (1.1 ** (1 / (365 * 86400)) - 1) * 1e18 +) # 10% if PegKeepers are empty, 4% when at target fraction SIGMA = 2 * 10**16 # 2% when at target debt fraction DEBT_FRACTION = 10 * 10**16 # 10% @@ -37,22 +42,23 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): mpolicy = account.deploy( - project.AggMonetaryPolicy2, - ADMIN, - PRICE_ORACLE, - CONTROLLER_FACTORY, - PEG_KEEPERS, - RATE, - SIGMA, - DEBT_FRACTION, - **kw) - print(mpolicy.rate('0x8472A9A7632b173c8Cf3a86D3afec50c35548e76')) + project.AggMonetaryPolicy2, + ADMIN, + PRICE_ORACLE, + CONTROLLER_FACTORY, + PEG_KEEPERS, + RATE, + SIGMA, + DEBT_FRACTION, + **kw, + ) + print(mpolicy.rate("0x8472A9A7632b173c8Cf3a86D3afec50c35548e76")) diff --git a/scripts/ape-monetary-policy-weth-wbtc.py b/scripts/ape-monetary-policy-weth-wbtc.py index 68c2bade..bedf2a30 100644 --- a/scripts/ape-monetary-policy-weth-wbtc.py +++ b/scripts/ape-monetary-policy-weth-wbtc.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -15,16 +17,22 @@ PRICE_ORACLE = "0x18672b1b0c623a30089A280Ed9256379fb0E4E62" # Agg CONTROLLER_FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" PEG_KEEPERS = [ - '0xaA346781dDD7009caa644A4980f044C50cD2ae22', - '0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8', - '0x6B765d07cf966c745B340AdCa67749fE75B5c345', - '0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae', - '0x0000000000000000000000000000000000000000'] -RATE = int((1.06**(1 / (365 * 86400)) - 1) * 1e18) # 6% if PegKeepers are empty, 2.2% when at target fraction + "0xaA346781dDD7009caa644A4980f044C50cD2ae22", + "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", + "0x6B765d07cf966c745B340AdCa67749fE75B5c345", + "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", + "0x0000000000000000000000000000000000000000", +] +RATE = int( + (1.06 ** (1 / (365 * 86400)) - 1) * 1e18 +) # 6% if PegKeepers are empty, 2.2% when at target fraction SIGMA = 2 * 10**16 # 2% when at target debt fraction DEBT_FRACTION = 10 * 10**16 # 10% -MARKETS = {'WBTC': "0x4e59541306910ad6dc1dac0ac9dfb29bd9f15c67", 'ETH': "0xa920de414ea4ab66b97da1bfe9e6eca7d4219635"} +MARKETS = { + "WBTC": "0x4e59541306910ad6dc1dac0ac9dfb29bd9f15c67", + "ETH": "0xa920de414ea4ab66b97da1bfe9e6eca7d4219635", +} @click.group() @@ -39,26 +47,29 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): for i, market in enumerate(MARKETS.keys()): mpolicy = account.deploy( - project.AggMonetaryPolicy2, - ADMIN, - PRICE_ORACLE, - CONTROLLER_FACTORY, - PEG_KEEPERS, - RATE, - SIGMA, - DEBT_FRACTION, - **kw) - print('Market:', market) - print('Policy:', mpolicy.address) - print('Rate:', (1 + mpolicy.rate(MARKETS[market]) / 1e18) ** (365 * 86400) - 1) + project.AggMonetaryPolicy2, + ADMIN, + PRICE_ORACLE, + CONTROLLER_FACTORY, + PEG_KEEPERS, + RATE, + SIGMA, + DEBT_FRACTION, + **kw, + ) + print("Market:", market) + print("Policy:", mpolicy.address) + print( + "Rate:", (1 + mpolicy.rate(MARKETS[market]) / 1e18) ** (365 * 86400) - 1 + ) print() diff --git a/scripts/ape-oracle-deploy.py b/scripts/ape-oracle-deploy.py index db70efef..d14ae9dd 100644 --- a/scripts/ape-oracle-deploy.py +++ b/scripts/ape-oracle-deploy.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -29,23 +31,24 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): account.deploy( - project.CryptoWithStablePriceAndChainlinkFrxeth, - TRICRYPTO, # Tricrypto - 1, # price index with ETH - "0x6F333fD5642eBc96040dF684489Db550e788E9Ed", # old USDT pool - FRXETH_POOL, # staked swap (frxeth/eth) - "0x832f436Ad2813c76AAe756703CAcB5c1028d11Da", # Agg - CHAINLINK_ETH, # ETH/USD Chainlink aggregator - SFRXETH, # sFrxETH - 600, - 1, # 1% bound - **kw) + project.CryptoWithStablePriceAndChainlinkFrxeth, + TRICRYPTO, # Tricrypto + 1, # price index with ETH + "0x6F333fD5642eBc96040dF684489Db550e788E9Ed", # old USDT pool + FRXETH_POOL, # staked swap (frxeth/eth) + "0x832f436Ad2813c76AAe756703CAcB5c1028d11Da", # Agg + CHAINLINK_ETH, # ETH/USD Chainlink aggregator + SFRXETH, # sFrxETH + 600, + 1, # 1% bound + **kw, + ) diff --git a/scripts/ape-steth-oracle.py b/scripts/ape-steth-oracle.py index 35168caf..612f7541 100644 --- a/scripts/ape-steth-oracle.py +++ b/scripts/ape-steth-oracle.py @@ -1,28 +1,48 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) STABLECOIN = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" AGG_SIGMA = 10**15 -ALL_CRVUSD_POOLS = ['0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E', '0x390f3595bCa2Df7d23783dFd126427CCeb997BF4', '0xCa978A0528116DDA3cbA9ACD3e68bc6191CA53D0', '0x34D655069F4cAc1547E4C8cA284FfFF5ad4A8db0'] +ALL_CRVUSD_POOLS = [ + "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4", + "0xCa978A0528116DDA3cbA9ACD3e68bc6191CA53D0", + "0x34D655069F4cAc1547E4C8cA284FfFF5ad4A8db0", +] OWNERSHIP_ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" -PEG_KEEPERS = ['0xaA346781dDD7009caa644A4980f044C50cD2ae22', '0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8', '0x6B765d07cf966c745B340AdCa67749fE75B5c345', '0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae'] +PEG_KEEPERS = [ + "0xaA346781dDD7009caa644A4980f044C50cD2ae22", + "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", + "0x6B765d07cf966c745B340AdCa67749fE75B5c345", + "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", +] ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -policy_rate = int((1.09**(1 / (365 * 86400)) - 1) * 1e18) # 9% if PegKeepers are empty, 3.6% when at target fraction +policy_rate = int( + (1.09 ** (1 / (365 * 86400)) - 1) * 1e18 +) # 9% if PegKeepers are empty, 3.6% when at target fraction policy_sigma = 2 * 10**16 # 2% when at target debt fraction policy_debt_fraction = 10 * 10**16 # 10% -TRICRYPTO = ["0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4"] # USDC, USDT -CRVUSD_POOLS = ["0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", "0x390f3595bca2df7d23783dfd126427cceb997bf4"] # USDC, USDT +TRICRYPTO = [ + "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", + "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4", +] # USDC, USDT +CRVUSD_POOLS = [ + "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "0x390f3595bca2df7d23783dfd126427cceb997bf4", +] # USDC, USDT STETH_POOL = "0x21e27a5e5513d6e65c4f830167390997aa84843a" WSTETH = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" @@ -43,50 +63,57 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): agg = account.deploy( - project.AggregateStablePrice2, - STABLECOIN, - AGG_SIGMA, - account, - **kw) + project.AggregateStablePrice2, STABLECOIN, AGG_SIGMA, account, **kw + ) for pool in ALL_CRVUSD_POOLS: agg.add_price_pair(pool, **kw) agg.set_admin(OWNERSHIP_ADMIN, **kw) - policy = account.deploy(project.AggMonetaryPolicy2, OWNERSHIP_ADMIN, agg, FACTORY, - PEG_KEEPERS + [ZERO_ADDRESS], - policy_rate, policy_sigma, policy_debt_fraction, - **kw) + policy = account.deploy( + project.AggMonetaryPolicy2, + OWNERSHIP_ADMIN, + agg, + FACTORY, + PEG_KEEPERS + [ZERO_ADDRESS], + policy_rate, + policy_sigma, + policy_debt_fraction, + **kw, + ) oracle = account.deploy( - project.CryptoWithStablePriceWstethN, - TRICRYPTO, - [1, 1], # price index with ETH - CRVUSD_POOLS, # CRVUSD stableswaps - STETH_POOL, # staked swap (steth/eth) - agg, - FACTORY, - WSTETH, - CHAINLINK_ETH, - CHAINLINK_STETH, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceWstethN, + TRICRYPTO, + [1, 1], # price index with ETH + CRVUSD_POOLS, # CRVUSD stableswaps + STETH_POOL, # staked swap (steth/eth) + agg, + FACTORY, + WSTETH, + CHAINLINK_ETH, + CHAINLINK_STETH, + BOUND_SIZE, + **kw, + ) agg.price_w(**kw) oracle.price_w(**kw) - policy.rate_write('0x8472A9A7632b173c8Cf3a86D3afec50c35548e76', **kw) - - print('========================') - print('Price Aggregator: ', agg.address) - print('Aggregator price: ', agg.price() / 1e18) - print('Monetary policy: ', policy.address) - print('Sample rate: ', policy.rate('0x8472A9A7632b173c8Cf3a86D3afec50c35548e76')) - print('Price Oracle: ', oracle.address) + policy.rate_write("0x8472A9A7632b173c8Cf3a86D3afec50c35548e76", **kw) + + print("========================") + print("Price Aggregator: ", agg.address) + print("Aggregator price: ", agg.price() / 1e18) + print("Monetary policy: ", policy.address) + print( + "Sample rate: ", policy.rate("0x8472A9A7632b173c8Cf3a86D3afec50c35548e76") + ) + print("Price Oracle: ", oracle.address) diff --git a/scripts/ape-steth-test-oracle.py b/scripts/ape-steth-test-oracle.py index 4af805a8..50b93755 100644 --- a/scripts/ape-steth-test-oracle.py +++ b/scripts/ape-steth-test-oracle.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -34,24 +36,25 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): account.deploy( - project.CryptoWithStablePriceWsteth, - TRICRYPTO_USDT, - 1, # price index with ETH - CRVUSD_USDT, # CRVUSD/USDT - STETH_POOL, # staked swap (steth/eth) - AGG, - FACTORY, - WSTETH, - CHAINLINK_ETH, - CHAINLINK_STETH, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceWsteth, + TRICRYPTO_USDT, + 1, # price index with ETH + CRVUSD_USDT, # CRVUSD/USDT + STETH_POOL, # staked swap (steth/eth) + AGG, + FACTORY, + WSTETH, + CHAINLINK_ETH, + CHAINLINK_STETH, + BOUND_SIZE, + **kw, + ) diff --git a/scripts/ape-tbtc-oracle.py b/scripts/ape-tbtc-oracle.py index f6a195a1..8004e53b 100644 --- a/scripts/ape-tbtc-oracle.py +++ b/scripts/ape-tbtc-oracle.py @@ -7,6 +7,7 @@ from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -33,26 +34,27 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): oracle = account.deploy( - project.CryptoWithStablePriceTBTC, - TRICRYPTO, - 0, # price index with TBTC - AGG, - FACTORY, - CHAINLINK_BTC, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceTBTC, + TRICRYPTO, + 0, # price index with TBTC + AGG, + FACTORY, + CHAINLINK_BTC, + BOUND_SIZE, + **kw, + ) oracle.price_w(**kw) - print('========================') - print('Price Oracle: ', oracle.address) - print('Price: ', oracle.price() / 1e18) + print("========================") + print("Price Oracle: ", oracle.address) + print("Price: ", oracle.price() / 1e18) diff --git a/scripts/ape-test-deploy.py b/scripts/ape-test-deploy.py index d75e8046..5b5714da 100644 --- a/scripts/ape-test-deploy.py +++ b/scripts/ape-test-deploy.py @@ -2,11 +2,13 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -17,15 +19,15 @@ def deploy_blueprint(contract, account, **kw): initcode = bytes.fromhex(initcode.removeprefix("0x")) initcode = b"\xfe\x71\x00" + initcode # eip-5202 preamble version 0 initcode = ( - b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode + b"\x61" + + len(initcode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + initcode ) if not kw: - kw = {'gas_price': project.provider.gas_price} + kw = {"gas_price": project.provider.gas_price} tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - nonce=account.nonce, - **kw + chain_id=project.provider.chain_id, data=initcode, nonce=account.nonce, **kw ) receipt = account.call(tx) click.echo(f"blueprint deployed at: {receipt.contract_address}") @@ -44,12 +46,12 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): # account.deploy(project.DummyPriceOracle, account, 2000 * 10**18, **kw) diff --git a/scripts/ape-wbtc-oracle.py b/scripts/ape-wbtc-oracle.py index 93377f7b..d7d18339 100644 --- a/scripts/ape-wbtc-oracle.py +++ b/scripts/ape-wbtc-oracle.py @@ -1,10 +1,12 @@ from ape import project, accounts, networks from ape.cli import NetworkBoundCommand, network_option + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -12,11 +14,22 @@ STABLECOIN = "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" OWNERSHIP_ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" -PEG_KEEPERS = ['0xaA346781dDD7009caa644A4980f044C50cD2ae22', '0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8', '0x6B765d07cf966c745B340AdCa67749fE75B5c345', '0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae'] +PEG_KEEPERS = [ + "0xaA346781dDD7009caa644A4980f044C50cD2ae22", + "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", + "0x6B765d07cf966c745B340AdCa67749fE75B5c345", + "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", +] ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -TRICRYPTO = ["0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4"] # USDC, USDT -CRVUSD_POOLS = ["0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", "0x390f3595bca2df7d23783dfd126427cceb997bf4"] # USDC, USDT +TRICRYPTO = [ + "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", + "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4", +] # USDC, USDT +CRVUSD_POOLS = [ + "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "0x390f3595bca2df7d23783dfd126427cceb997bf4", +] # USDC, USDT FACTORY = "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC" CHAINLINK_BTC = "0xf4030086522a5beea4988f8ca5b36dbc97bee88c" BOUND_SIZE = int(0.015 * 1e18) @@ -36,27 +49,28 @@ def cli(): ) @network_option() def deploy(network): - account = accounts.load('babe') + account = accounts.load("babe") account.set_autosign(True) max_fee = networks.active_provider.base_fee * 2 max_priority_fee = int(0.5e9) - kw = {'max_fee': max_fee, 'max_priority_fee': max_priority_fee} + kw = {"max_fee": max_fee, "max_priority_fee": max_priority_fee} with accounts.use_sender(account): oracle = account.deploy( - project.CryptoWithStablePriceWBTC, - TRICRYPTO, - [0, 0], # price index with WBTC - CRVUSD_POOLS, # CRVUSD stableswaps - AGG, - FACTORY, - CHAINLINK_BTC, - BOUND_SIZE, - **kw) + project.CryptoWithStablePriceWBTC, + TRICRYPTO, + [0, 0], # price index with WBTC + CRVUSD_POOLS, # CRVUSD stableswaps + AGG, + FACTORY, + CHAINLINK_BTC, + BOUND_SIZE, + **kw, + ) oracle.price_w(**kw) - print('========================') - print('Price Oracle: ', oracle.address) - print('Price: ', oracle.price() / 1e18) + print("========================") + print("Price Oracle: ", oracle.address) + print("Price: ", oracle.price() / 1e18) diff --git a/scripts/arbi-agg-deployer.py b/scripts/arbi-agg-deployer.py index b428b7cd..6fb317a9 100644 --- a/scripts/arbi-agg-deployer.py +++ b/scripts/arbi-agg-deployer.py @@ -19,29 +19,31 @@ "0xec090cf6DD891D2d014beA6edAda6e05E025D93d", # USDC "0x73aF1150F265419Ef8a5DB41908B700C32D49135", # USDT "0x3aDf984c937FA6846E5a24E0A68521Bdaf767cE1", # USDC.e - "0x2FE7AE43591E534C256A1594D326e5779E302Ff4" # FRAX + "0x2FE7AE43591E534C256A1594D326e5779E302Ff4", # FRAX ] admin = "0x452030a5D962d37D97A9D65487663cD5fd9C2B32" def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(ARBITRUM) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - agg_factory = boa.load_partial('contracts/price_oracles/AggregateStablePrice3.vy') + agg_factory = boa.load_partial("contracts/price_oracles/AggregateStablePrice3.vy") agg = agg_factory.deploy(crvusd, sigma, boa.env.eoa) for pool in pools: diff --git a/scripts/boa-arbi-console.py b/scripts/boa-arbi-console.py index caf3df8f..e2456daa 100755 --- a/scripts/boa-arbi-console.py +++ b/scripts/boa-arbi-console.py @@ -13,24 +13,33 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(ARBITRUM) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - oracle_code = boa.load_partial('contracts/price_oracles/L2/CryptoFromPoolArbitrum.vy') - agg_oracle_code = boa.load_partial('contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy') - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy').at(FACTORY) + oracle_code = boa.load_partial( + "contracts/price_oracles/L2/CryptoFromPoolArbitrum.vy" + ) + agg_oracle_code = boa.load_partial( + "contracts/price_oracles/L2/CryptoFromPoolsRateArbitrumWAgg.vy" + ) + factory = boa.load_partial( + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy" + ).at(FACTORY) import IPython + IPython.embed() diff --git a/scripts/boa-deploy-1inch-leverage-zap.py b/scripts/boa-deploy-1inch-leverage-zap.py index 400594cf..c89e24cf 100644 --- a/scripts/boa-deploy-1inch-leverage-zap.py +++ b/scripts/boa-deploy-1inch-leverage-zap.py @@ -10,36 +10,45 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) + CONSTANTS = { "mainnet": { "rpc": ETHEREUM, "router_1inch": "0x111111125421ca6dc452d289314280a0f8842a65", - "factories": ["0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0", "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC"], # LlamaLend, crvUSD + "factories": [ + "0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0", + "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + ], # LlamaLend, crvUSD }, "arbitrum": { "rpc": ARBITRUM, "router_1inch": "0x111111125421ca6dc452d289314280a0f8842a65", "factories": ["0xcaEC110C784c9DF37240a8Ce096D352A75922DeA"], - } + }, } -if __name__ == '__main__': - if '--network' not in sys.argv[1:]: +if __name__ == "__main__": + if "--network" not in sys.argv[1:]: raise Exception("You must pass '--network' arg") if sys.argv[2] not in CONSTANTS: raise Exception(f"{sys.argv[2]} network is not supported") network = sys.argv[2] boa.set_network_env(CONSTANTS[network]["rpc"]) - boa.env.add_account(account_load('curve-deployer')) + boa.env.add_account(account_load("curve-deployer")) boa.env._fork_try_prefetch_state = False - contract = boa.load('contracts/zaps/LeverageZap.vy', - CONSTANTS[network]["router_1inch"], CONSTANTS[network]["factories"]) + contract = boa.load( + "contracts/zaps/LeverageZap.vy", + CONSTANTS[network]["router_1inch"], + CONSTANTS[network]["factories"], + ) - print('Deployed at:', contract.address) + print("Deployed at:", contract.address) diff --git a/scripts/boa-deploy-flashloan.py b/scripts/boa-deploy-flashloan.py index 4584c48b..c50ce243 100644 --- a/scripts/boa-deploy-flashloan.py +++ b/scripts/boa-deploy-flashloan.py @@ -11,17 +11,19 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': +if __name__ == "__main__": boa.set_network_env(ETHEREUM) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - contract = boa.load('contracts/flashloan/FlashLender.vy', CRVUSD_FACTORY) + contract = boa.load("contracts/flashloan/FlashLender.vy", CRVUSD_FACTORY) - print('Deployed at:', contract.address) + print("Deployed at:", contract.address) diff --git a/scripts/boa-deploy-fxn-oracle.py b/scripts/boa-deploy-fxn-oracle.py index b9934cdc..a32065c3 100755 --- a/scripts/boa-deploy-fxn-oracle.py +++ b/scripts/boa-deploy-fxn-oracle.py @@ -12,29 +12,37 @@ from networks import ARBITRUM -POOLS = ["0x5f0985A8aAd85e82fD592a23Cc0501e4345fb18c", "0x82670f35306253222F8a165869B28c64739ac62e"] # FXN/WETH, Tricrypto(crvUSD+WBTC+WETH) +POOLS = [ + "0x5f0985A8aAd85e82fD592a23Cc0501e4345fb18c", + "0x82670f35306253222F8a165869B28c64739ac62e", +] # FXN/WETH, Tricrypto(crvUSD+WBTC+WETH) BORROWED_IXS = [0, 0] COLLATERAL_IXS = [1, 2] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(ARBITRUM) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False contract = boa.load( - 'contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy', - POOLS, BORROWED_IXS, COLLATERAL_IXS) + "contracts/price_oracles/L2/CryptoFromPoolsRateArbitrum.vy", + POOLS, + BORROWED_IXS, + COLLATERAL_IXS, + ) - print('Deployed at:', contract.address) + print("Deployed at:", contract.address) diff --git a/scripts/boa-deploy-lending-example.py b/scripts/boa-deploy-lending-example.py index 53d596a0..3a9fa469 100755 --- a/scripts/boa-deploy-lending-example.py +++ b/scripts/boa-deploy-lending-example.py @@ -9,36 +9,57 @@ NETWORK = "http://localhost:8545" ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" CRVUSD = "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e" -HARDHAT_COMMAND = ["npx", "hardhat", "node", "--fork", "https://eth.drpc.org", "--port", "8545"] +HARDHAT_COMMAND = [ + "npx", + "hardhat", + "node", + "--fork", + "https://eth.drpc.org", + "--port", + "8545", +] -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() - vault_impl = boa.load('contracts/lending/Vault.vy') - price_oracle_impl = boa.load_partial('contracts/price_oracles/CryptoFromPool.vy').deploy_as_blueprint() - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy').deploy_as_blueprint() - gauge_impl = boa.load_partial('contracts/lending/LiquidityGauge.vy').deploy_as_blueprint() + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() + vault_impl = boa.load("contracts/lending/Vault.vy") + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/CryptoFromPool.vy" + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy" + ).deploy_as_blueprint() + gauge_impl = boa.load_partial( + "contracts/lending/LiquidityGauge.vy" + ).deploy_as_blueprint() factory = boa.load( - 'contracts/lending/OneWayLendingFactory.vy', - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, gauge_impl, - ADMIN) + "contracts/lending/OneWayLendingFactory.vy", + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + gauge_impl, + ADMIN, + ) # Deploying an example CRV = "0xd533a949740bb3306d119cc777fa900ba034cd52" TRICRV_POOL = "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14" - factory.create_from_pool(CRVUSD, CRV, 100, int(0.006 * 1e18), 9 * 10**16, 6 * 10**16, TRICRV_POOL) - vault_compiled = boa.load_partial('contracts/lending/Vault.vy') + factory.create_from_pool( + CRVUSD, CRV, 100, int(0.006 * 1e18), 9 * 10**16, 6 * 10**16, TRICRV_POOL + ) + vault_compiled = boa.load_partial("contracts/lending/Vault.vy") vault = vault_compiled.at(factory.vaults(0)) amm_address = vault.amm() controller_address = vault.controller() @@ -46,28 +67,37 @@ erc20_compiled = boa.load_partial("contracts/testing/ERC20Mock.vy") crv = erc20_compiled.at(CRV) - crv.transfer("0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", 10**24, sender="0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2") + crv.transfer( + "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", + 10**24, + sender="0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2", + ) crvusd = erc20_compiled.at(CRVUSD) - crvusd.transfer("0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", 10**24, sender="0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635") + crvusd.transfer( + "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", + 10**24, + sender="0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + ) - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Gauge implementation:', gauge_impl.address) - print('Factory:', factory.address) - print('--------------------------') - print('Vault:', vault.address) - print('AMM:', amm_address) - print('Controller:', controller_address) - print('Price Oracle:', price_oracle_address) - print('==========================') + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Gauge implementation:", gauge_impl.address) + print("Factory:", factory.address) + print("--------------------------") + print("Vault:", vault.address) + print("AMM:", amm_address) + print("Controller:", controller_address) + print("Price Oracle:", price_oracle_address) + print("==========================") import IPython + IPython.embed() - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/boa-deploy-lending.py b/scripts/boa-deploy-lending.py index 2093a3e6..cbf2948d 100755 --- a/scripts/boa-deploy-lending.py +++ b/scripts/boa-deploy-lending.py @@ -14,59 +14,80 @@ NETWORK = "http://localhost:8545" ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" CRVUSD = "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e" -HARDHAT_COMMAND = ["npx", "hardhat", "node", "--fork", "https://eth.drpc.org", "--port", "8545"] +HARDHAT_COMMAND = [ + "npx", + "hardhat", + "node", + "--fork", + "https://eth.drpc.org", + "--port", + "8545", +] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() - vault_impl = boa.load('contracts/lending/Vault.vy') - price_oracle_impl = boa.load_partial('contracts/price_oracles/CryptoFromPool.vy').deploy_as_blueprint() - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy').deploy_as_blueprint() - gauge_impl = boa.load_partial('contracts/lending/LiquidityGauge.vy').deploy_as_blueprint() + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() + vault_impl = boa.load("contracts/lending/Vault.vy") + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/CryptoFromPool.vy" + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy" + ).deploy_as_blueprint() + gauge_impl = boa.load_partial( + "contracts/lending/LiquidityGauge.vy" + ).deploy_as_blueprint() factory = boa.load( - 'contracts/lending/OneWayLendingFactory.vy', - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, gauge_impl, - ADMIN) - - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Gauge implementation:', gauge_impl.address) - print('Factory:', factory.address) - print('==========================') - - if '--markets' in sys.argv[1:]: + "contracts/lending/OneWayLendingFactory.vy", + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + gauge_impl, + ADMIN, + ) + + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Gauge implementation:", gauge_impl.address) + print("Factory:", factory.address) + print("==========================") + + if "--markets" in sys.argv[1:]: # Deploy wstETH long market name = "wstETH-long" oracle_pool = "0x2889302a794dA87fBF1D6Db415C1492194663D13" # TricryptoLLAMA - collateral = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" # wstETH + collateral = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" # wstETH borrowed = CRVUSD A = 70 fee = int(0.002 * 1e18) @@ -74,15 +95,25 @@ def account_load(fname): liquidation_discount = int(0.04 * 1e18) min_borrow_rate = 5 * 10**15 // (365 * 86400) # 0.5% max_borrow_rate = 80 * 10**16 // (365 * 86400) # 80% - vault = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) gauge = factory.deploy_gauge(vault) print(f"Vault {name}: {vault}, gauge: {gauge}") # Deploy WETH long market name = "WETH-long" oracle_pool = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" # TriCRV - collateral = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH + collateral = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # WETH borrowed = CRVUSD A = 70 fee = int(0.002 * 1e18) @@ -90,15 +121,25 @@ def account_load(fname): liquidation_discount = int(0.04 * 1e18) min_borrow_rate = 5 * 10**15 // (365 * 86400) # 0.5% max_borrow_rate = 80 * 10**16 // (365 * 86400) # 80% - vault = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) gauge = factory.deploy_gauge(vault) print(f"Vault {name}: {vault}, gauge: {gauge}") # Deploy tBTC long market name = "tBTC-long" oracle_pool = "0x2889302a794dA87fBF1D6Db415C1492194663D13" # TricryptoLLAMA - collateral = "0x18084fbA666a33d37592fA2633fD49a74DD93a88" # tBTC + collateral = "0x18084fbA666a33d37592fA2633fD49a74DD93a88" # tBTC borrowed = CRVUSD A = 75 fee = int(0.0015 * 1e18) @@ -106,8 +147,18 @@ def account_load(fname): liquidation_discount = int(0.035 * 1e18) min_borrow_rate = 5 * 10**15 // (365 * 86400) # 0.5% max_borrow_rate = 80 * 10**16 // (365 * 86400) # 80% - vault = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) gauge = factory.deploy_gauge(vault) print(f"Vault {name}: {vault}, gauge: {gauge}") @@ -121,8 +172,18 @@ def account_load(fname): fee = int(0.002 * 1e18) borrowing_discount = int(0.11 * 1e18) liquidation_discount = int(0.08 * 1e18) - vault = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) gauge = factory.deploy_gauge(vault) print(f"Vault {name}: {vault}, gauge: {gauge}") @@ -131,10 +192,20 @@ def account_load(fname): oracle_pool = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" # TriCRV collateral = CRVUSD borrowed = CRV - vault = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) gauge = factory.deploy_gauge(vault) print(f"Vault {name}: {vault}, gauge: {gauge}") - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/boa-deploy-odos-leverage-zap.py b/scripts/boa-deploy-odos-leverage-zap.py index 5e2b5cfb..70aa08c3 100644 --- a/scripts/boa-deploy-odos-leverage-zap.py +++ b/scripts/boa-deploy-odos-leverage-zap.py @@ -10,16 +10,22 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) + CONSTANTS = { "mainnet": { "rpc": ETHEREUM, "router_odos": "0xCf5540fFFCdC3d510B18bFcA6d2b9987b0772559", - "factories": ["0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0", "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC"], # LlamaLend, crvUSD + "factories": [ + "0xeA6876DDE9e3467564acBeE1Ed5bac88783205E0", + "0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC", + ], # LlamaLend, crvUSD }, "optimism": { "rpc": OPTIMISM, @@ -40,21 +46,24 @@ def account_load(fname): "rpc": SONIC, "router_odos": "0xaC041Df48dF9791B0654f1Dbbf2CC8450C5f2e9D", "factories": ["0x30d1859dad5a52ae03b6e259d1b48c4b12933993"], - } + }, } -if __name__ == '__main__': - if '--network' not in sys.argv[1:]: +if __name__ == "__main__": + if "--network" not in sys.argv[1:]: raise Exception("You must pass '--network' arg") if sys.argv[2] not in CONSTANTS: raise Exception(f"{sys.argv[2]} network is not supported") network = sys.argv[2] boa.set_network_env(CONSTANTS[network]["rpc"]) - boa.env.add_account(account_load('curve-deployer')) + boa.env.add_account(account_load("curve-deployer")) boa.env._fork_try_prefetch_state = False - contract = boa.load('contracts/zaps/LeverageZap.vy', - CONSTANTS[network]["router_odos"], CONSTANTS[network]["factories"]) + contract = boa.load( + "contracts/zaps/LeverageZap.vy", + CONSTANTS[network]["router_odos"], + CONSTANTS[network]["factories"], + ) - print('Deployed at:', contract.address) + print("Deployed at:", contract.address) diff --git a/scripts/boa-deploy-susde-oracle.py b/scripts/boa-deploy-susde-oracle.py index 255239fb..69b1d323 100755 --- a/scripts/boa-deploy-susde-oracle.py +++ b/scripts/boa-deploy-susde-oracle.py @@ -19,23 +19,30 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False contract = boa.load( - 'contracts/price_oracles/CryptoFromPoolVault_noncurve.vy', - POOL, 2, BORROWED_IX, COLLATERAL_IX, VAULT) - - print('Deployed at:', contract.address) + "contracts/price_oracles/CryptoFromPoolVault_noncurve.vy", + POOL, + 2, + BORROWED_IX, + COLLATERAL_IX, + VAULT, + ) + + print("Deployed at:", contract.address) diff --git a/scripts/boa-market-creation-console.py b/scripts/boa-market-creation-console.py index 8e4a465d..11a24d73 100755 --- a/scripts/boa-market-creation-console.py +++ b/scripts/boa-market-creation-console.py @@ -25,22 +25,25 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactory.vy').at(FACTORY) + factory = boa.load_partial("contracts/lending/OneWayLendingFactory.vy").at(FACTORY) import IPython + IPython.embed() diff --git a/scripts/boa-monetary-policy-3.py b/scripts/boa-monetary-policy-3.py index df014bfb..dc83cbf0 100755 --- a/scripts/boa-monetary-policy-3.py +++ b/scripts/boa-monetary-policy-3.py @@ -19,45 +19,48 @@ "0xaA346781dDD7009caa644A4980f044C50cD2ae22", "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", "0x6B765d07cf966c745B340AdCa67749fE75B5c345", - "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae" + "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/ControllerFactory.vy').at(FACTORY) + factory = boa.load_partial("contracts/ControllerFactory.vy").at(FACTORY) contract = boa.load( - 'contracts/mpolicies/AggMonetaryPolicy3.vy', + "contracts/mpolicies/AggMonetaryPolicy3.vy", ADMIN, STABLE_ORACLE, FACTORY, PEG_KEEPERS + [ZERO_ADDRESS], RATE0, 2 * 10**16, # Sigma 2% - 10 * 10**16) # Target debt fraction 10% + 10 * 10**16, + ) # Target debt fraction 10% - print('Deployed at:', contract.address) + print("Deployed at:", contract.address) for i in range(50000): controller = factory.controllers(i) if controller == "0x0000000000000000000000000000000000000000": break - print('Saving candle for:', controller) + print("Saving candle for:", controller) contract.rate_write(controller, gas=10**6) contract.rate_write(gas=10**6) diff --git a/scripts/boa-new-amm-controller.py b/scripts/boa-new-amm-controller.py index 9f8f81ef..71fd3b2e 100755 --- a/scripts/boa-new-amm-controller.py +++ b/scripts/boa-new-amm-controller.py @@ -13,26 +13,28 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('==========================') + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("==========================") diff --git a/scripts/boa-salvation.py b/scripts/boa-salvation.py index bb78ab28..3bb1fa5c 100644 --- a/scripts/boa-salvation.py +++ b/scripts/boa-salvation.py @@ -9,10 +9,10 @@ from boa.network import NetworkEnv -RANSOM = 15 * 10 ** 6 * 10 ** 18 # ALTER: bank of crvUSD available +RANSOM = 15 * 10**6 * 10**18 # ALTER: bank of crvUSD available IDX = 3 # ALTER: coin to buy out (TUSD) -NETWORK = f"http://localhost:8545" # ALTER: provider +NETWORK = "http://localhost:8545" # ALTER: provider ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" POOLS = [ @@ -37,14 +37,18 @@ def _pool(idx): if POOLS[idx] not in _contracts: # _contracts[POOLS[idx]] = boa.load_partial("contracts/StableSwap.vy").at(POOLS[idx]) - _contracts[POOLS[idx]] = boa.from_etherscan(POOLS[idx], name="StableSwap", api_key=ETHERSCAN_API) + _contracts[POOLS[idx]] = boa.from_etherscan( + POOLS[idx], name="StableSwap", api_key=ETHERSCAN_API + ) return _contracts[POOLS[idx]] def _peg_keeper(idx): if PEG_KEEPERS[idx] not in _contracts: # _contracts[PEG_KEEPERS[idx]] = boa.load_partial("contracts/stabilizer/PegKeeper.vy").at(PEG_KEEPERS[idx]) - _contracts[PEG_KEEPERS[idx]] = boa.from_etherscan(PEG_KEEPERS[idx], name="PegKeeper", api_key=ETHERSCAN_API) + _contracts[PEG_KEEPERS[idx]] = boa.from_etherscan( + PEG_KEEPERS[idx], name="PegKeeper", api_key=ETHERSCAN_API + ) return _contracts[PEG_KEEPERS[idx]] @@ -52,7 +56,9 @@ def _coins(pool): coins = [pool] for coin in [pool.coins(0), CRVUSD]: if coin not in _contracts: - _contracts[coin] = boa.from_etherscan(pool.coins(0), name="coin", api_key=ETHERSCAN_API) + _contracts[coin] = boa.from_etherscan( + pool.coins(0), name="coin", api_key=ETHERSCAN_API + ) coins.append(_contracts[coin]) return coins @@ -64,7 +70,14 @@ def deploy(): return salvation.deploy() -def buy_out(idx=IDX, ransom=RANSOM, max_total_supply=None, max_price=None, salvation=None, use_all=False): +def buy_out( + idx=IDX, + ransom=RANSOM, + max_total_supply=None, + max_price=None, + salvation=None, + use_all=False, +): pool, pk = _pool(idx), _peg_keeper(idx) if not max_total_supply: max_total_supply = pool.totalSupply() * 1001 // 1000 @@ -74,23 +87,25 @@ def buy_out(idx=IDX, ransom=RANSOM, max_total_supply=None, max_price=None, salva salvation = deploy() coins = _coins(pool) - initial_balances = [ - coin.balanceOf(boa.env.eoa) for coin in coins - ] + initial_balances = [coin.balanceOf(boa.env.eoa) for coin in coins] - bought_out = salvation.buy_out(pool, pk, ransom, max_total_supply, max_price, use_all) - print(f"Bought out: {bought_out / 10 ** 18:>11.2f} crvUSD") - print(f"Remaining: {pk.debt() / 10 ** 18:>11.2f} crvUSD") + bought_out = salvation.buy_out( + pool, pk, ransom, max_total_supply, max_price, use_all + ) + print(f"Bought out: {bought_out / 10**18:>11.2f} crvUSD") + print(f"Remaining: {pk.debt() / 10**18:>11.2f} crvUSD") diffs = [] for coin, initial_balance in zip(coins, initial_balances): delimiter = 10 ** coin.decimals() new_balance = coin.balanceOf(boa.env.eoa) diff = new_balance - initial_balance - print(f"{coin.symbol()}: {initial_balance / delimiter:.2f} -> {new_balance / delimiter:.2f} " - f"({'+' if diff > 0 else ''}{diff / delimiter:.2f})") + print( + f"{coin.symbol()}: {initial_balance / delimiter:.2f} -> {new_balance / delimiter:.2f} " + f"({'+' if diff > 0 else ''}{diff / delimiter:.2f})" + ) diffs.append(diff) - print(f"Total: {diffs[1] / pool.price_oracle() + diffs[2] / 10 ** 18:.2f} crvUSD") + print(f"Total: {diffs[1] / pool.price_oracle() + diffs[2] / 10**18:.2f} crvUSD") def simulate(idx=IDX, ransom=RANSOM): @@ -98,11 +113,13 @@ def simulate(idx=IDX, ransom=RANSOM): salvation = deploy() to = boa.env.eoa if CRVUSD not in _contracts: - _contracts[CRVUSD] = boa.from_etherscan(CRVUSD, name="crvUSD", api_key=ETHERSCAN_API) + _contracts[CRVUSD] = boa.from_etherscan( + CRVUSD, name="crvUSD", api_key=ETHERSCAN_API + ) crvusd = _contracts[CRVUSD] for i in range(5): # ALTER: number of iterations - if _peg_keeper(idx).debt() < 10 ** 18: # Small amounts may fail due to fees + if _peg_keeper(idx).debt() < 10**18: # Small amounts may fail due to fees print("Peg Keeper is free") break @@ -124,20 +141,22 @@ def simulate(idx=IDX, ransom=RANSOM): def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" simulate() else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) # ALTER: account to use + boa.env.add_account(account_load("babe")) # ALTER: account to use boa.env._fork_try_prefetch_state = False buy_out() diff --git a/scripts/console_debug.py b/scripts/console_debug.py index 655c5675..8db11efb 100644 --- a/scripts/console_debug.py +++ b/scripts/console_debug.py @@ -10,19 +10,24 @@ def deploy_blueprint(contract, account, txparams={}): - txparams = {k: v for k, v in txparams.items() if k != 'from'} - bytecode = b"\xFE\x71\x00" + bytes.fromhex(contract.bytecode[2:]) - bytecode = b"\x61" + len(bytecode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + bytecode + txparams = {k: v for k, v in txparams.items() if k != "from"} + bytecode = b"\xfe\x71\x00" + bytes.fromhex(contract.bytecode[2:]) + bytecode = ( + b"\x61" + + len(bytecode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + bytecode + ) tx = account.transfer(data=bytecode, **txparams) return tx.contract_address def main(): - mainnet = network.show_active() == 'mainnet' + mainnet = network.show_active() == "mainnet" if mainnet: raise NotImplementedError("Mainnet not implemented yet") else: - txparams = {'from': accounts[0]} + txparams = {"from": accounts[0]} admin = accounts[0] fee_receiver = accounts[0] @@ -38,14 +43,20 @@ def main(): policy = ConstantMonetaryPolicy.deploy(admin, txparams) policy.set_rate(0, txparams) # 0% price_oracle = DummyPriceOracle.deploy(admin, 3000 * 10**18, txparams) - collateral_token = ERC20Mock.deploy('Collateral WETH', 'WETH', 18, txparams) + collateral_token = ERC20Mock.deploy("Collateral WETH", "WETH", 18, txparams) factory.add_market( - collateral_token, 100, 10**16, 0, + collateral_token, + 100, + 10**16, + 0, price_oracle, - policy, 5 * 10**16, 2 * 10**16, + policy, + 5 * 10**16, + 2 * 10**16, 10**6 * 10**18, - txparams) + txparams, + ) amm = AMM.at(factory.get_amm(collateral_token)) controller = Controller.at(factory.get_controller(collateral_token)) @@ -54,13 +65,16 @@ def main(): for user in accounts: collateral_token._mint_for_testing(user, 10**4 * 10**18, txparams) - print('========================') - print('Stablecoin: ', stablecoin.address) - print('Factory: ', factory.address) - print('Collateral: ', collateral_token.address) - print('AMM: ', amm.address) - print('Controller: ', controller.address) + print("========================") + print("Stablecoin: ", stablecoin.address) + print("Factory: ", factory.address) + print("Collateral: ", collateral_token.address) + print("AMM: ", amm.address) + print("Controller: ", controller.address) active_project = project.get_loaded_projects()[0] shell = console.Console(active_project, extra_locals=locals()) - shell.interact(banner="Deployment successful. Use `collateral_token`, `stablecoin`, `amm` and `controller` objects", exitmsg="") + shell.interact( + banner="Deployment successful. Use `collateral_token`, `stablecoin`, `amm` and `controller` objects", + exitmsg="", + ) diff --git a/scripts/create-sfrax.py b/scripts/create-sfrax.py index 40216ac4..96fa1d6a 100644 --- a/scripts/create-sfrax.py +++ b/scripts/create-sfrax.py @@ -41,30 +41,33 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactory.vy').at(FACTORY) + factory = boa.load_partial("contracts/lending/OneWayLendingFactory.vy").at(FACTORY) oracle = boa.load( - 'contracts/price_oracles/CryptoFromPoolVaultWAgg.vy', + "contracts/price_oracles/CryptoFromPoolVaultWAgg.vy", FRAX_POOL, 2, # Number of coins 1, # Borrowed index 0, # Collateral index SFRAX, - AGG) + AGG, + ) sleep(10) oracle.price_w() sleep(10) @@ -75,18 +78,18 @@ def account_load(fname): 285, # A int(0.002e18), # fee int(0.013e18), # loan_discount - int(0.010e18), # liq_discount + int(0.010e18), # liq_discount oracle.address, - 'sfrax-long', + "sfrax-long", min_rate, - max_rate + max_rate, ) sleep(10) gauge = factory.deploy_gauge(vault) sleep(10) - print(f'Vault: {vault}') - print(f'Gauge: {gauge}') - print(f'Oracle: {oracle.address}') - print(f'Oracle price: {oracle.price() / 1e18}') + print(f"Vault: {vault}") + print(f"Gauge: {gauge}") + print(f"Oracle: {oracle.address}") + print(f"Oracle price: {oracle.price() / 1e18}") print() diff --git a/scripts/create-usde.py b/scripts/create-usde.py index 49dbac84..60190bc2 100755 --- a/scripts/create-usde.py +++ b/scripts/create-usde.py @@ -37,29 +37,32 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactory.vy').at(FACTORY) + factory = boa.load_partial("contracts/lending/OneWayLendingFactory.vy").at(FACTORY) oracle = boa.load( - 'contracts/price_oracles/CryptoFromPoolWAgg.vy', + "contracts/price_oracles/CryptoFromPoolWAgg.vy", USDE_POOL, 2, # Number of coins 1, # Borrowed index 0, # Collateral index - AGG) + AGG, + ) sleep(10) oracle.price_w() sleep(10) @@ -70,18 +73,18 @@ def account_load(fname): 500, # A int(0.002e18), # fee int(0.015e18), # loan_discount - int(0.01e18), # liq_discount + int(0.01e18), # liq_discount oracle.address, - 'susde-long', + "susde-long", min_rate, - max_rate + max_rate, ) sleep(10) gauge = factory.deploy_gauge(vault) sleep(10) - print(f'Vault: {vault}') - print(f'Gauge: {gauge}') - print(f'Oracle: {oracle.address}') - print(f'Oracle price: {oracle.price() / 1e18}') + print(f"Vault: {vault}") + print(f"Gauge: {gauge}") + print(f"Oracle: {oracle.address}") + print(f"Oracle price: {oracle.price() / 1e18}") print() diff --git a/scripts/deploy-lending-arb-crv.py b/scripts/deploy-lending-arb-crv.py index f9d7cd90..8b17553a 100755 --- a/scripts/deploy-lending-arb-crv.py +++ b/scripts/deploy-lending-arb-crv.py @@ -21,51 +21,68 @@ CHAIN_ID = 42161 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(ARBITRUM) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy').at(FACTORY) + factory = boa.load_partial( + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy" + ).at(FACTORY) # Deploy CRV long market name = "CRV-long" oracle_pool = "0x845C8bc94610807fCbaB5dd2bc7aC9DAbaFf3c55" # TriCRV-ARBITRUM - collateral = "0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978" # CRV + collateral = "0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978" # CRV borrowed = CRVUSD # Same as on ETH mainnet A = 30 @@ -74,15 +91,25 @@ def account_load(fname): liquidation_discount = int(0.08 * 1e18) min_borrow_rate = 5 * 10**16 // (365 * 86400) # 5% max_borrow_rate = 60 * 10**16 // (365 * 86400) # 60% - vault_crv = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault_crv = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) salt_crv = os.urandom(32) gauge_factory.deploy_gauge(vault_crv, salt_crv, GAUGE_FUNDER) print(f"Vault {name}: {vault_crv}, salt: {salt_crv.hex()}") # Deploy ARB long market name = "ARB-long" - collateral = "0x912CE59144191C1204E64559FE8253a0e49E6548" # ARB + collateral = "0x912CE59144191C1204E64559FE8253a0e49E6548" # ARB borrowed = CRVUSD A = 30 fee = int(0.0015 * 1e18) @@ -90,26 +117,38 @@ def account_load(fname): liquidation_discount = int(0.08 * 1e18) min_borrow_rate = 5 * 10**16 // (365 * 86400) # 5% max_borrow_rate = 60 * 10**16 // (365 * 86400) # 60% - vault_arb = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault_arb = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) salt_arb = os.urandom(32) gauge_factory.deploy_gauge(vault_arb, salt_arb, GAUGE_FUNDER) print(f"Vault {name}: {vault_arb}, salt: {salt_arb.hex()}") - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY + ) gauge_factory_eth.deploy_gauge(CHAIN_ID, salt_crv) - if '--fork' not in sys.argv[1:]: + if "--fork" not in sys.argv[1:]: sleep(30) # RPCs on Ethereum can change the node, so need to sleep to not fail gauge_factory_eth.deploy_gauge(CHAIN_ID, salt_arb) - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/deploy-lending-arbitrum.py b/scripts/deploy-lending-arbitrum.py index 61d0e9cf..1ebae4c7 100755 --- a/scripts/deploy-lending-arbitrum.py +++ b/scripts/deploy-lending-arbitrum.py @@ -21,72 +21,98 @@ CHAIN_ID = 42161 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(ARBITRUM) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() - vault_impl = boa.load('contracts/lending/Vault.vy') - price_oracle_impl = boa.load_partial('contracts/price_oracles/L2/CryptoFromPoolArbitrum.vy').deploy_as_blueprint() - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy').deploy_as_blueprint() - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() + vault_impl = boa.load("contracts/lending/Vault.vy") + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/L2/CryptoFromPoolArbitrum.vy" + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy" + ).deploy_as_blueprint() + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, - ADMIN) - - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Factory:', factory.address) - print('==========================') - - if '--markets' in sys.argv[1:]: + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy", + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + GAUGE_FACTORY, + ADMIN, + ) + + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Factory:", factory.address) + print("==========================") + + if "--markets" in sys.argv[1:]: # Deploy WETH long market name = "WETH-long" - oracle_pool = "0x82670f35306253222F8a165869B28c64739ac62e" # Tricrypto-crvUSD (Arbitrum) - collateral = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH + oracle_pool = ( + "0x82670f35306253222F8a165869B28c64739ac62e" # Tricrypto-crvUSD (Arbitrum) + ) + collateral = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH borrowed = CRVUSD A = 70 fee = int(0.006 * 1e18) @@ -94,16 +120,28 @@ def account_load(fname): liquidation_discount = int(0.04 * 1e18) min_borrow_rate = 2 * 10**16 // (365 * 86400) # 2% max_borrow_rate = 50 * 10**16 // (365 * 86400) # 50% - vault_weth = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault_weth = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) salt_weth = os.urandom(32) gauge_factory.deploy_gauge(vault_weth, salt_weth, GAUGE_FUNDER) print(f"Vault {name}: {vault_weth}, salt: {salt_weth.hex()}") # Deploy wBTC long market name = "wBTC-long" - oracle_pool = "0x82670f35306253222F8a165869B28c64739ac62e" # Tricrypto-crvUSD (Arbitrum) - collateral = "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f" # wBTC + oracle_pool = ( + "0x82670f35306253222F8a165869B28c64739ac62e" # Tricrypto-crvUSD (Arbitrum) + ) + collateral = "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f" # wBTC borrowed = CRVUSD A = 75 fee = int(0.006 * 1e18) @@ -111,26 +149,40 @@ def account_load(fname): liquidation_discount = int(0.035 * 1e18) min_borrow_rate = 2 * 10**16 // (365 * 86400) # 2% max_borrow_rate = 50 * 10**16 // (365 * 86400) # 50% - vault_wbtc = factory.create_from_pool(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - oracle_pool, name, min_borrow_rate, max_borrow_rate) + vault_wbtc = factory.create_from_pool( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + oracle_pool, + name, + min_borrow_rate, + max_borrow_rate, + ) salt_wbtc = os.urandom(32) gauge_factory.deploy_gauge(vault_wbtc, salt_wbtc, GAUGE_FUNDER) print(f"Vault {name}: {vault_wbtc}, salt: {salt_wbtc.hex()}") - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY + ) gauge_factory_eth.deploy_gauge(CHAIN_ID, salt_weth) - if '--fork' not in sys.argv[1:]: - sleep(30) # RPCs on Ethereum can change the node, so need to sleep to not fail + if "--fork" not in sys.argv[1:]: + sleep( + 30 + ) # RPCs on Ethereum can change the node, so need to sleep to not fail gauge_factory_eth.deploy_gauge(CHAIN_ID, salt_wbtc) - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/deploy-lending-fraxtal.py b/scripts/deploy-lending-fraxtal.py index 052ef052..f5d2dca0 100644 --- a/scripts/deploy-lending-fraxtal.py +++ b/scripts/deploy-lending-fraxtal.py @@ -20,143 +20,191 @@ HARDHAT_COMMAND = ["npx", "hardhat", "node", "--fork", FRAXTAL, "--port", "8545"] MARKET_PARAMS = [ - ('sfrxETH', { - 'collateral': '0xFC00000000000000000000000000000000000005', - 'A': 70, - 'fee': int(0.015e18), - 'borrowing_discount': int(0.07e18), - 'liquidation_discount': int(0.04e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0xF97c707024ef0DD3E77a0824555a46B622bfB500', - 'supply_limit': 2**256-1 - }), - ('sFRAX', { - 'collateral': '0xfc00000000000000000000000000000000000008', - 'A': 285, - 'fee': int(0.015e18), - 'borrowing_discount': int(0.02e18), - 'liquidation_discount': int(0.015e18), - 'min_borrow_rate': 5 * 10**15 // (365 * 86400), - 'max_borrow_rate': 15 * 10**16 // (365 * 86400), - 'oracle_contract': '0x960ea3e3C7FB317332d990873d354E18d7645590', - 'supply_limit': 2**256-1 - }), - ('FXS', { - 'collateral': '0xFc00000000000000000000000000000000000002', - 'A': 22, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.155e18), - 'liquidation_discount': int(0.125e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x8e0B8c8BB9db49a46697F3a5Bb8A308e744821D2', - 'supply_limit': 15 * 10**6 * 10**18 - }), - ('CRV', { - 'collateral': '0x331B9182088e2A7d6D3Fe4742AbA1fB231aEcc56', - 'A': 22, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.155e18), - 'liquidation_discount': int(0.125e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x48A68C5511DfC355007b7B794890F26653A7bF93', - 'supply_limit': 15 * 10**6 * 10**18 - }) + ( + "sfrxETH", + { + "collateral": "0xFC00000000000000000000000000000000000005", + "A": 70, + "fee": int(0.015e18), + "borrowing_discount": int(0.07e18), + "liquidation_discount": int(0.04e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0xF97c707024ef0DD3E77a0824555a46B622bfB500", + "supply_limit": 2**256 - 1, + }, + ), + ( + "sFRAX", + { + "collateral": "0xfc00000000000000000000000000000000000008", + "A": 285, + "fee": int(0.015e18), + "borrowing_discount": int(0.02e18), + "liquidation_discount": int(0.015e18), + "min_borrow_rate": 5 * 10**15 // (365 * 86400), + "max_borrow_rate": 15 * 10**16 // (365 * 86400), + "oracle_contract": "0x960ea3e3C7FB317332d990873d354E18d7645590", + "supply_limit": 2**256 - 1, + }, + ), + ( + "FXS", + { + "collateral": "0xFc00000000000000000000000000000000000002", + "A": 22, + "fee": int(0.006e18), + "borrowing_discount": int(0.155e18), + "liquidation_discount": int(0.125e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x8e0B8c8BB9db49a46697F3a5Bb8A308e744821D2", + "supply_limit": 15 * 10**6 * 10**18, + }, + ), + ( + "CRV", + { + "collateral": "0x331B9182088e2A7d6D3Fe4742AbA1fB231aEcc56", + "A": 22, + "fee": int(0.006e18), + "borrowing_discount": int(0.155e18), + "liquidation_discount": int(0.125e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x48A68C5511DfC355007b7B794890F26653A7bF93", + "supply_limit": 15 * 10**6 * 10**18, + }, + ), ] CHAIN_ID = 252 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(FRAXTAL) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(FRAXTAL) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() - vault_impl = boa.load('contracts/lending/Vault.vy') - price_oracle_impl = boa.load_partial('contracts/price_oracles/CryptoFromPool.vy').deploy_as_blueprint() # XXX - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy').deploy_as_blueprint() - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() + vault_impl = boa.load("contracts/lending/Vault.vy") + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/CryptoFromPool.vy" + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy" + ).deploy_as_blueprint() + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, - ADMIN) - - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Factory:', factory.address) - print('==========================') - - if '--markets' in sys.argv[1:]: + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy", + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + GAUGE_FACTORY, + ADMIN, + ) + + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Factory:", factory.address) + print("==========================") + + if "--markets" in sys.argv[1:]: for name, p in MARKET_PARAMS: vault = factory.create( - CRVUSD, p['collateral'], - p['A'], p['fee'], p['borrowing_discount'], p['liquidation_discount'], - p['oracle_contract'], name + '-long', - p['min_borrow_rate'], p['max_borrow_rate'], p['supply_limit']) + CRVUSD, + p["collateral"], + p["A"], + p["fee"], + p["borrowing_discount"], + p["liquidation_discount"], + p["oracle_contract"], + name + "-long", + p["min_borrow_rate"], + p["max_borrow_rate"], + p["supply_limit"], + ) salt = os.urandom(32) gauge_factory.deploy_gauge(vault, salt, GAUGE_FUNDER) print(f"Vault {name}: {vault}, salt: {salt.hex()}") - p['salt'] = salt + p["salt"] = salt - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY + ) for name, p in MARKET_PARAMS: - if '--fork' not in sys.argv[1:]: - sleep(30) # RPCs on Ethereum can change the node, so need to sleep to not fail - salt = p['salt'] - print(f'Deploying on Ethereum with salt: {salt.hex()}') + if "--fork" not in sys.argv[1:]: + sleep( + 30 + ) # RPCs on Ethereum can change the node, so need to sleep to not fail + salt = p["salt"] + print(f"Deploying on Ethereum with salt: {salt.hex()}") gauge_factory_eth.deploy_gauge(CHAIN_ID, salt) - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/deploy-lending-fxn.py b/scripts/deploy-lending-fxn.py index be7bf9a8..e38a23bb 100644 --- a/scripts/deploy-lending-fxn.py +++ b/scripts/deploy-lending-fxn.py @@ -24,50 +24,67 @@ CHAIN_ID = 42161 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(ARBITRUM) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy').at(FACTORY) + factory = boa.load_partial( + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy" + ).at(FACTORY) # Deploy FXN long market name = "FXN-long" - collateral = "0x179F38f78346F5942E95C5C59CB1da7F55Cf7CAd" # FXN + collateral = "0x179F38f78346F5942E95C5C59CB1da7F55Cf7CAd" # FXN borrowed = CRVUSD A = 21 fee = int(0.015 * 1e18) @@ -75,23 +92,35 @@ def account_load(fname): liquidation_discount = int(0.14 * 1e18) min_borrow_rate = 5 * 10**15 // (365 * 86400) # 0.5% max_borrow_rate = 60 * 10**16 // (365 * 86400) # 60% - vault_fxn = factory.create(borrowed, collateral, A, fee, borrowing_discount, liquidation_discount, - ORACLE, name, min_borrow_rate, max_borrow_rate) + vault_fxn = factory.create( + borrowed, + collateral, + A, + fee, + borrowing_discount, + liquidation_discount, + ORACLE, + name, + min_borrow_rate, + max_borrow_rate, + ) salt_fxn = os.urandom(32) gauge_factory.deploy_gauge(vault_fxn, salt_fxn, GAUGE_FUNDER) print(f"Vault {name}: {vault_fxn}, salt: {salt_fxn.hex()}") - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY + ) gauge_factory_eth.deploy_gauge(CHAIN_ID, salt_fxn) - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/deploy-lending-optimism.py b/scripts/deploy-lending-optimism.py index 3c7d0bc0..45f83a2e 100644 --- a/scripts/deploy-lending-optimism.py +++ b/scripts/deploy-lending-optimism.py @@ -26,154 +26,205 @@ # ] MARKET_PARAMS = [ - ('ETH', { - 'collateral': '0x4200000000000000000000000000000000000006', - 'A': 70, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.07e18), - 'liquidation_discount': int(0.04e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x92577943c7aC4accb35288aB2CC84D75feC330aF', - 'supply_limit': 2**256-1 - }), - ('wstETH', { - 'collateral': '0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb', - 'A': 70, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.07e18), - 'liquidation_discount': int(0.04e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x44343B1B95BaA53eC561F8d7B357155B89507077', - 'supply_limit': 2**256-1 - }), - ('WBTC', { - 'collateral': '0x68f180fcCe6836688e9084f035309E29Bf0A2095', - 'A': 70, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.065e18), - 'liquidation_discount': int(0.035e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0xEc12C072d9ABdf3F058C8B17169eED334fC1dE58', - 'supply_limit': 2**256-1 - }), - ('OP', { - 'collateral': '0x4200000000000000000000000000000000000042', - 'A': 22, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.155e18), - 'liquidation_discount': int(0.125e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x3Fa8ebd5d16445b42e0b6A54678718C94eA99aBC', - 'supply_limit': 15 * 10**6 * 10**18 - }), - ('CRV', { - 'collateral': '0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53', - 'A': 22, - 'fee': int(0.006e18), - 'borrowing_discount': int(0.155e18), - 'liquidation_discount': int(0.125e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 30 * 10**16 // (365 * 86400), - 'oracle_contract': '0x2016f1AaE491438E6EA908e30b60dAeb56ac185c', - 'supply_limit': 15 * 10**6 * 10**18 - }) + ( + "ETH", + { + "collateral": "0x4200000000000000000000000000000000000006", + "A": 70, + "fee": int(0.006e18), + "borrowing_discount": int(0.07e18), + "liquidation_discount": int(0.04e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x92577943c7aC4accb35288aB2CC84D75feC330aF", + "supply_limit": 2**256 - 1, + }, + ), + ( + "wstETH", + { + "collateral": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", + "A": 70, + "fee": int(0.006e18), + "borrowing_discount": int(0.07e18), + "liquidation_discount": int(0.04e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x44343B1B95BaA53eC561F8d7B357155B89507077", + "supply_limit": 2**256 - 1, + }, + ), + ( + "WBTC", + { + "collateral": "0x68f180fcCe6836688e9084f035309E29Bf0A2095", + "A": 70, + "fee": int(0.006e18), + "borrowing_discount": int(0.065e18), + "liquidation_discount": int(0.035e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0xEc12C072d9ABdf3F058C8B17169eED334fC1dE58", + "supply_limit": 2**256 - 1, + }, + ), + ( + "OP", + { + "collateral": "0x4200000000000000000000000000000000000042", + "A": 22, + "fee": int(0.006e18), + "borrowing_discount": int(0.155e18), + "liquidation_discount": int(0.125e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x3Fa8ebd5d16445b42e0b6A54678718C94eA99aBC", + "supply_limit": 15 * 10**6 * 10**18, + }, + ), + ( + "CRV", + { + "collateral": "0x0994206dfE8De6Ec6920FF4D779B0d950605Fb53", + "A": 22, + "fee": int(0.006e18), + "borrowing_discount": int(0.155e18), + "liquidation_discount": int(0.125e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 30 * 10**16 // (365 * 86400), + "oracle_contract": "0x2016f1AaE491438E6EA908e30b60dAeb56ac185c", + "supply_limit": 15 * 10**6 * 10**18, + }, + ), ] CHAIN_ID = 10 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--hardhat' in sys.argv[1:]: +if __name__ == "__main__": + if "--hardhat" in sys.argv[1:]: hardhat = subprocess.Popen(HARDHAT_COMMAND) sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(OPTIMISM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(OPTIMISM) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy').deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy').deploy_as_blueprint() - vault_impl = boa.load('contracts/lending/Vault.vy') - price_oracle_impl = boa.load_partial('contracts/price_oracles/L2/CryptoFromPoolOptimismWAgg.vy').deploy_as_blueprint() - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy').deploy_as_blueprint() - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) + amm_impl = boa.load_partial("contracts/AMM.vy").deploy_as_blueprint() + controller_impl = boa.load_partial("contracts/Controller.vy").deploy_as_blueprint() + vault_impl = boa.load("contracts/lending/Vault.vy") + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/L2/CryptoFromPoolOptimismWAgg.vy" + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy" + ).deploy_as_blueprint() + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) factory = boa.load( - 'contracts/lending/OneWayLendingFactoryL2.vy', - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, - ADMIN) - - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Factory:', factory.address) - print('==========================') - - if '--markets' in sys.argv[1:]: + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy", + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + GAUGE_FACTORY, + ADMIN, + ) + + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Factory:", factory.address) + print("==========================") + + if "--markets" in sys.argv[1:]: for name, p in MARKET_PARAMS: vault = factory.create( - CRVUSD, p['collateral'], - p['A'], p['fee'], p['borrowing_discount'], p['liquidation_discount'], - p['oracle_contract'], name + '-long', - p['min_borrow_rate'], p['max_borrow_rate'], p['supply_limit']) + CRVUSD, + p["collateral"], + p["A"], + p["fee"], + p["borrowing_discount"], + p["liquidation_discount"], + p["oracle_contract"], + name + "-long", + p["min_borrow_rate"], + p["max_borrow_rate"], + p["supply_limit"], + ) salt = os.urandom(32) gauge_factory.deploy_gauge(vault, salt, GAUGE_FUNDER) print(f"Vault {name}: {vault}, salt: {salt.hex()}") - p['salt'] = salt + p["salt"] = salt - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY + ) for name, p in MARKET_PARAMS: - if '--fork' not in sys.argv[1:]: - sleep(30) # RPCs on Ethereum can change the node, so need to sleep to not fail - salt = p['salt'] - print(f'Deploying on Ethereum with salt: {salt.hex()}') + if "--fork" not in sys.argv[1:]: + sleep( + 30 + ) # RPCs on Ethereum can change the node, so need to sleep to not fail + salt = p["salt"] + print(f"Deploying on Ethereum with salt: {salt.hex()}") gauge_factory_eth.deploy_gauge(CHAIN_ID, salt) - if '--hardhat' in sys.argv[1:]: + if "--hardhat" in sys.argv[1:]: hardhat.wait() diff --git a/scripts/deploy-lending-sonic.py b/scripts/deploy-lending-sonic.py index 851166e5..14b98de4 100644 --- a/scripts/deploy-lending-sonic.py +++ b/scripts/deploy-lending-sonic.py @@ -24,159 +24,225 @@ # S, wETH, stS, oS MARKET_PARAMS = [ - ('wS', { - 'collateral': '0x039e2fb66102314ce7b64ce5ce3e5183bc94ad38', - 'A': 17, - 'fee': int(0.005e18), - 'borrowing_discount': int(0.17e18), - 'liquidation_discount': int(0.14e18), - 'min_borrow_rate': 1 * 10**16 // (365 * 86400), - 'max_borrow_rate': 60 * 10**16 // (365 * 86400), - 'oracle_contract': '0x1daB6560494B04473A0BE3E7D83CF3Fdf3a51828', - 'supply_limit': 2**256-1 - }), - ('stS', { - 'collateral': '0xE5DA20F15420aD15DE0fa650600aFc998bbE3955', - 'A': 17, - 'fee': int(0.005e18), - 'borrowing_discount': int(0.17e18), - 'liquidation_discount': int(0.14e18), - 'min_borrow_rate': 1 * 10**16 // (365 * 86400), - 'max_borrow_rate': 60 * 10**16 // (365 * 86400), - 'oracle_contract': '0x58e57cA18B7A47112b877E31929798Cd3D703b0f', - 'supply_limit': 2**256-1 - }), - ('wOS', { - 'collateral': '0x9F0dF7799f6FDAd409300080cfF680f5A23df4b1', - 'A': 17, - 'fee': int(0.005e18), - 'borrowing_discount': int(0.17e18), - 'liquidation_discount': int(0.14e18), - 'min_borrow_rate': 1 * 10**16 // (365 * 86400), - 'max_borrow_rate': 60 * 10**16 // (365 * 86400), - 'oracle_contract': '0x3a1659Ddcf2339Be3aeA159cA010979FB49155FF', - 'supply_limit': 2**256-1 - }), - ('scETH', { - 'collateral': '0x3bcE5CB273F0F148010BbEa2470e7b5df84C7812', - 'A': 70, - 'fee': int(0.005e18), - 'borrowing_discount': int(0.07e18), - 'liquidation_discount': int(0.04e18), - 'min_borrow_rate': 2 * 10**16 // (365 * 86400), - 'max_borrow_rate': 40 * 10**16 // (365 * 86400), - 'oracle_contract': '0x2F0AF8eC2f5893392843a0F647A30A141dba9DaF', - # 'oracle_contract': '0xB755B949C126C04e0348DD881a5cF55d424742B2', wETH - 'supply_limit': 2**256-1 - }), - ('scUSD', { - 'collateral': '0xd3DCe716f3eF535C5Ff8d041c1A41C3bd89b97aE', # Params same as USDe - 'A': 500, - 'fee': int(0.001e18), - 'borrowing_discount': int(0.015e18), - 'liquidation_discount': int(0.01e18), - 'min_borrow_rate': 1 * 10**16 // (365 * 86400), - 'max_borrow_rate': 35 * 10**16 // (365 * 86400), - 'oracle_contract': '0x48A68C5511DfC355007b7B794890F26653A7bF93', # Usually 1.0 - 'supply_limit': 2**256-1 - }), + ( + "wS", + { + "collateral": "0x039e2fb66102314ce7b64ce5ce3e5183bc94ad38", + "A": 17, + "fee": int(0.005e18), + "borrowing_discount": int(0.17e18), + "liquidation_discount": int(0.14e18), + "min_borrow_rate": 1 * 10**16 // (365 * 86400), + "max_borrow_rate": 60 * 10**16 // (365 * 86400), + "oracle_contract": "0x1daB6560494B04473A0BE3E7D83CF3Fdf3a51828", + "supply_limit": 2**256 - 1, + }, + ), + ( + "stS", + { + "collateral": "0xE5DA20F15420aD15DE0fa650600aFc998bbE3955", + "A": 17, + "fee": int(0.005e18), + "borrowing_discount": int(0.17e18), + "liquidation_discount": int(0.14e18), + "min_borrow_rate": 1 * 10**16 // (365 * 86400), + "max_borrow_rate": 60 * 10**16 // (365 * 86400), + "oracle_contract": "0x58e57cA18B7A47112b877E31929798Cd3D703b0f", + "supply_limit": 2**256 - 1, + }, + ), + ( + "wOS", + { + "collateral": "0x9F0dF7799f6FDAd409300080cfF680f5A23df4b1", + "A": 17, + "fee": int(0.005e18), + "borrowing_discount": int(0.17e18), + "liquidation_discount": int(0.14e18), + "min_borrow_rate": 1 * 10**16 // (365 * 86400), + "max_borrow_rate": 60 * 10**16 // (365 * 86400), + "oracle_contract": "0x3a1659Ddcf2339Be3aeA159cA010979FB49155FF", + "supply_limit": 2**256 - 1, + }, + ), + ( + "scETH", + { + "collateral": "0x3bcE5CB273F0F148010BbEa2470e7b5df84C7812", + "A": 70, + "fee": int(0.005e18), + "borrowing_discount": int(0.07e18), + "liquidation_discount": int(0.04e18), + "min_borrow_rate": 2 * 10**16 // (365 * 86400), + "max_borrow_rate": 40 * 10**16 // (365 * 86400), + "oracle_contract": "0x2F0AF8eC2f5893392843a0F647A30A141dba9DaF", + # 'oracle_contract': '0xB755B949C126C04e0348DD881a5cF55d424742B2', wETH + "supply_limit": 2**256 - 1, + }, + ), + ( + "scUSD", + { + "collateral": "0xd3DCe716f3eF535C5Ff8d041c1A41C3bd89b97aE", # Params same as USDe + "A": 500, + "fee": int(0.001e18), + "borrowing_discount": int(0.015e18), + "liquidation_discount": int(0.01e18), + "min_borrow_rate": 1 * 10**16 // (365 * 86400), + "max_borrow_rate": 35 * 10**16 // (365 * 86400), + "oracle_contract": "0x48A68C5511DfC355007b7B794890F26653A7bF93", # Usually 1.0 + "supply_limit": 2**256 - 1, + }, + ), ] CHAIN_ID = 146 GAUGE_FACTORY_ABI = [ - {"stateMutability": "nonpayable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_lp_token", "type": "address"}, {"name": "_salt", "type": "bytes32"}, {"name": "_manager", "type": "address"}], - "outputs": [{"name": "", "type": "address"}]}, + { + "stateMutability": "nonpayable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_lp_token", "type": "address"}, + {"name": "_salt", "type": "bytes32"}, + {"name": "_manager", "type": "address"}, + ], + "outputs": [{"name": "", "type": "address"}], + }, ] GAUGE_FACTORY_ABI_ETH = [ - {"stateMutability": "payable", - "type": "function", - "name": "deploy_gauge", - "inputs": [{"name": "_chain_id", "type": "uint256"}, {"name": "_salt", "type": "bytes32"}], - "outputs": [{"name": "", "type": "address"}], - "gas": 165352} + { + "stateMutability": "payable", + "type": "function", + "name": "deploy_gauge", + "inputs": [ + {"name": "_chain_id", "type": "uint256"}, + {"name": "_salt", "type": "bytes32"}, + ], + "outputs": [{"name": "", "type": "address"}], + "gas": 165352, + } ] -SONIC_ARGS = {'settings': CompilerSettings(evm_version='shanghai', optimize=OptimizationLevel.CODESIZE)} +SONIC_ARGS = { + "settings": CompilerSettings( + evm_version="shanghai", optimize=OptimizationLevel.CODESIZE + ) +} def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(SONIC) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(SONIC) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - amm_impl = boa.load_partial('contracts/AMM.vy', compiler_args=SONIC_ARGS).deploy_as_blueprint() - controller_impl = boa.load_partial('contracts/Controller.vy', compiler_args=SONIC_ARGS).deploy_as_blueprint() - vault_impl = boa.load_partial('contracts/lending/Vault.vy', compiler_args=SONIC_ARGS).deploy() - price_oracle_impl = boa.load_partial('contracts/price_oracles/CryptoFromPool.vy', compiler_args=SONIC_ARGS).deploy_as_blueprint() - mpolicy_impl = boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy', compiler_args=SONIC_ARGS).deploy_as_blueprint() - gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at(GAUGE_FACTORY) - - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy', compiler_args=SONIC_ARGS).deploy( - CRVUSD, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, GAUGE_FACTORY, - ADMIN) - - print('Deployed contracts:') - print('==========================') - print('AMM implementation:', amm_impl.address) - print('Controller implementation:', controller_impl.address) - print('Vault implementation:', vault_impl.address) - print('Pool price oracle implementation:', price_oracle_impl.address) - print('Monetary Policy implementation:', mpolicy_impl.address) - print('Factory:', factory.address) - print('==========================') + amm_impl = boa.load_partial( + "contracts/AMM.vy", compiler_args=SONIC_ARGS + ).deploy_as_blueprint() + controller_impl = boa.load_partial( + "contracts/Controller.vy", compiler_args=SONIC_ARGS + ).deploy_as_blueprint() + vault_impl = boa.load_partial( + "contracts/lending/Vault.vy", compiler_args=SONIC_ARGS + ).deploy() + price_oracle_impl = boa.load_partial( + "contracts/price_oracles/CryptoFromPool.vy", compiler_args=SONIC_ARGS + ).deploy_as_blueprint() + mpolicy_impl = boa.load_partial( + "contracts/mpolicies/SemilogMonetaryPolicy.vy", compiler_args=SONIC_ARGS + ).deploy_as_blueprint() + gauge_factory = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI).at( + GAUGE_FACTORY + ) + + factory = boa.load_partial( + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy", + compiler_args=SONIC_ARGS, + ).deploy( + CRVUSD, + amm_impl, + controller_impl, + vault_impl, + price_oracle_impl, + mpolicy_impl, + GAUGE_FACTORY, + ADMIN, + ) + + print("Deployed contracts:") + print("==========================") + print("AMM implementation:", amm_impl.address) + print("Controller implementation:", controller_impl.address) + print("Vault implementation:", vault_impl.address) + print("Pool price oracle implementation:", price_oracle_impl.address) + print("Monetary Policy implementation:", mpolicy_impl.address) + print("Factory:", factory.address) + print("==========================") sleep(5) - if '--markets' in sys.argv[1:]: + if "--markets" in sys.argv[1:]: for idx, (name, p) in enumerate(MARKET_PARAMS): factory.create( - CRVUSD, p['collateral'], - p['A'], p['fee'], p['borrowing_discount'], p['liquidation_discount'], - p['oracle_contract'], name + '-long', - p['min_borrow_rate'], p['max_borrow_rate'], p['supply_limit'], - gas=15_000_000) + CRVUSD, + p["collateral"], + p["A"], + p["fee"], + p["borrowing_discount"], + p["liquidation_discount"], + p["oracle_contract"], + name + "-long", + p["min_borrow_rate"], + p["max_borrow_rate"], + p["supply_limit"], + gas=15_000_000, + ) sleep(5) vault = factory.vaults(idx) salt = os.urandom(32) try: gauge_factory.deploy_gauge(vault, salt, GAUGE_FUNDER, gas=1_000_000) except Exception as e: - print(f'Error {e} is not error?') + print(f"Error {e} is not error?") print(f"Vault {name}: {vault}, salt: {salt.hex()}") - p['salt'] = salt + p["salt"] = salt sleep(5) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at(GAUGE_FACTORY_ETH) + gauge_factory_eth = ABIContractFactory.from_abi_dict(GAUGE_FACTORY_ABI_ETH).at( + GAUGE_FACTORY_ETH + ) for name, p in MARKET_PARAMS: - salt = p['salt'] - print(f'Deploying on Ethereum {name} with salt: {salt.hex()}') + salt = p["salt"] + print(f"Deploying on Ethereum {name} with salt: {salt.hex()}") try: gauge_factory_eth.deploy_gauge(CHAIN_ID, salt) except Exception as e: - print(f'Maybe {e} is also success? All RPCs are complete shit, get Vitalik on the line') - if '--fork' not in sys.argv[1:]: - sleep(250) # RPCs on Ethereum can change the node, so need to sleep to not fail + print( + f"Maybe {e} is also success? All RPCs are complete shit, get Vitalik on the line" + ) + if "--fork" not in sys.argv[1:]: + sleep( + 250 + ) # RPCs on Ethereum can change the node, so need to sleep to not fail diff --git a/scripts/deploy-secondary-mp-susde.py b/scripts/deploy-secondary-mp-susde.py index 26c8ff11..84024bf4 100755 --- a/scripts/deploy-secondary-mp-susde.py +++ b/scripts/deploy-secondary-mp-susde.py @@ -25,23 +25,33 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - policy = boa.load('contracts/mpolicies/SusdeMonetaryPolicy.vy', - FACTORY, SUSDE, BORROWED_TOKEN, U_0, LOW_RATIO, HIGH_RATIO, 0) + policy = boa.load( + "contracts/mpolicies/SusdeMonetaryPolicy.vy", + FACTORY, + SUSDE, + BORROWED_TOKEN, + U_0, + LOW_RATIO, + HIGH_RATIO, + 0, + ) print(policy.raw_susde_apr() / 1e18) print(policy.rate(CONTROLLER) * 86400 * 365 / 1e18) diff --git a/scripts/deploy-secondary-mps.py b/scripts/deploy-secondary-mps.py index 53053008..cac31c71 100755 --- a/scripts/deploy-secondary-mps.py +++ b/scripts/deploy-secondary-mps.py @@ -29,34 +29,52 @@ # WBTC, WETH, TBTC, WSTETH (== WETH) NAMES = ["WBTC", "WETH", "TBTC", "WSTETH"] # CRVUSD AMMS, not LlamaLend!! -AMMS = ["0xE0438Eb3703bF871E31Ce639bd351109c88666ea", "0x1681195C176239ac5E72d9aeBaCf5b2492E0C4ee", - "0xf9bD9da2427a50908C4c6D1599D8e62837C2BCB0", "0x1681195C176239ac5E72d9aeBaCf5b2492E0C4ee"] +AMMS = [ + "0xE0438Eb3703bF871E31Ce639bd351109c88666ea", + "0x1681195C176239ac5E72d9aeBaCf5b2492E0C4ee", + "0xf9bD9da2427a50908C4c6D1599D8e62837C2BCB0", + "0x1681195C176239ac5E72d9aeBaCf5b2492E0C4ee", +] SHIFTS = [0, 0, 0, int(4e16 / 365 / 86400)] # LlamaLend controllers -CONTROLLERS = ["0xcaD85b7fe52B1939DCEebEe9bCf0b2a5Aa0cE617", "0xaade9230AA9161880E13a38C83400d3D1995267b", - "0x413FD2511BAD510947a91f5c6c79EBD8138C29Fc", "0x1E0165DbD2019441aB7927C018701f3138114D71"] +CONTROLLERS = [ + "0xcaD85b7fe52B1939DCEebEe9bCf0b2a5Aa0cE617", + "0xaade9230AA9161880E13a38C83400d3D1995267b", + "0x413FD2511BAD510947a91f5c6c79EBD8138C29Fc", + "0x1E0165DbD2019441aB7927C018701f3138114D71", +] def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_env(NetworkEnv(NETWORK)) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False policies = [ - boa.load('contracts/mpolicies/SecondaryMonetaryPolicy.vy', - FACTORY, amm, BORROWED_TOKEN, U_0, LOW_RATIO, HIGH_RATIO, shift) + boa.load( + "contracts/mpolicies/SecondaryMonetaryPolicy.vy", + FACTORY, + amm, + BORROWED_TOKEN, + U_0, + LOW_RATIO, + HIGH_RATIO, + shift, + ) for (amm, shift) in zip(AMMS, SHIFTS) ] diff --git a/scripts/deploy.py b/scripts/deploy.py index c626ac09..3ddc14c0 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -8,19 +8,24 @@ def deploy_blueprint(contract, account, txparams={}): - txparams = {k: v for k, v in txparams.items() if k != 'from'} - bytecode = b"\xFE\x71\x00" + bytes.fromhex(contract.bytecode[2:]) - bytecode = b"\x61" + len(bytecode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + bytecode + txparams = {k: v for k, v in txparams.items() if k != "from"} + bytecode = b"\xfe\x71\x00" + bytes.fromhex(contract.bytecode[2:]) + bytecode = ( + b"\x61" + + len(bytecode).to_bytes(2, "big") + + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + bytecode + ) tx = account.transfer(data=bytecode, **txparams) return tx.contract_address def main(): - mainnet = network.show_active() == 'mainnet' + mainnet = network.show_active() == "mainnet" if mainnet: raise NotImplementedError("Mainnet not implemented yet") else: - txparams = {'from': accounts[0]} + txparams = {"from": accounts[0]} admin = accounts[0] fee_receiver = accounts[0] @@ -37,14 +42,20 @@ def main(): policy = ConstantMonetaryPolicy.deploy(admin, txparams) policy.set_rate(0, txparams) # 0% price_oracle = DummyPriceOracle.deploy(admin, 3000 * 10**18, txparams) - collateral_token = ERC20Mock.deploy('Collateral WETH', 'WETH', 18, txparams) + collateral_token = ERC20Mock.deploy("Collateral WETH", "WETH", 18, txparams) factory.add_market( - collateral_token, 100, 10**16, 0, + collateral_token, + 100, + 10**16, + 0, price_oracle, - policy, 5 * 10**16, 2 * 10**16, + policy, + 5 * 10**16, + 2 * 10**16, 10**6 * 10**18, - txparams) + txparams, + ) amm = AMM.at(factory.get_amm(collateral_token)) controller = Controller.at(factory.get_controller(collateral_token)) @@ -53,9 +64,9 @@ def main(): for user in accounts: collateral_token._mint_for_testing(user, 10**4 * 10**18, txparams) - print('========================') - print('Stablecoin: ', stablecoin.address) - print('Factory: ', factory.address) - print('Collateral: ', collateral_token.address) - print('AMM: ', amm.address) - print('Controller: ', controller.address) + print("========================") + print("Stablecoin: ", stablecoin.address) + print("Factory: ", factory.address) + print("Collateral: ", collateral_token.address) + print("AMM: ", amm.address) + print("Controller: ", controller.address) diff --git a/scripts/opti-agg-deployer.py b/scripts/opti-agg-deployer.py index f1751315..3f144bcc 100644 --- a/scripts/opti-agg-deployer.py +++ b/scripts/opti-agg-deployer.py @@ -25,22 +25,24 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(OPTIMISM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(OPTIMISM) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - agg_factory = boa.load_partial('contracts/price_oracles/AggregateStablePrice3.vy') + agg_factory = boa.load_partial("contracts/price_oracles/AggregateStablePrice3.vy") agg = agg_factory.deploy(crvusd, sigma, boa.env.eoa) for pool in pools: diff --git a/scripts/oracle-creation-console.py b/scripts/oracle-creation-console.py index 9f0358e2..bb2e8b2e 100644 --- a/scripts/oracle-creation-console.py +++ b/scripts/oracle-creation-console.py @@ -16,30 +16,41 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - oracle_factory = boa.load_partial('contracts/price_oracles/CryptoFromPoolsRate.vy') - factory_agg = boa.load_partial('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy') - - print('================================================================================================================') - print('Usage:') - print('>> oracle_factory.deploy([0xpool1, 0xpool2], [borrowed_ix1, borrowed_ix2], [collateral_ix1, collateral_ix2])') - print('>> factory_agg.deploy([0xpool1, 0xpool2], [borrowed_ix1, borrowed_ix2], [collateral_ix1, collateral_ix2], agg)') - print('================================================================================================================') + oracle_factory = boa.load_partial("contracts/price_oracles/CryptoFromPoolsRate.vy") + factory_agg = boa.load_partial("contracts/price_oracles/CryptoFromPoolsRateWAgg.vy") + + print( + "================================================================================================================" + ) + print("Usage:") + print( + ">> oracle_factory.deploy([0xpool1, 0xpool2], [borrowed_ix1, borrowed_ix2], [collateral_ix1, collateral_ix2])" + ) + print( + ">> factory_agg.deploy([0xpool1, 0xpool2], [borrowed_ix1, borrowed_ix2], [collateral_ix1, collateral_ix2], agg)" + ) + print( + "================================================================================================================" + ) print() import IPython + IPython.embed() diff --git a/scripts/recreate-arbi-markets.py b/scripts/recreate-arbi-markets.py index 7dde5b6f..f586cf32 100755 --- a/scripts/recreate-arbi-markets.py +++ b/scripts/recreate-arbi-markets.py @@ -25,38 +25,57 @@ 0: (0.01, 0.25), # ETH, market at 9.5% 1: (0.01, 0.25), # WBTC, market at 8.2% 5: (0.02, 0.30), # ARB, market at 11% - 4: (0.05, 0.40) # CRV + 4: (0.05, 0.40), # CRV } def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - babe_raw = account_load('babe') +if __name__ == "__main__": + babe_raw = account_load("babe") - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(ARBITRUM) boa.env.add_account(babe_raw) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactoryL2.vy').at(FACTORY) - gauge_factory = boa.from_etherscan(factory.gauge_factory(), name="GaugeFactory", uri="https://api.arbiscan.io/api", api_key=ARBISCAN_API_KEY) + factory = boa.load_partial( + "contracts/lending/deprecated/OneWayLendingFactoryL2.vy" + ).at(FACTORY) + gauge_factory = boa.from_etherscan( + factory.gauge_factory(), + name="GaugeFactory", + uri="https://api.arbiscan.io/api", + api_key=ARBISCAN_API_KEY, + ) gauges = {} salts = {} for ix in INDEXES: - old_controller = boa.from_etherscan(factory.controllers(ix), name="Controller", uri="https://api.arbiscan.io/api", api_key=ARBISCAN_API_KEY) - old_amm = boa.from_etherscan(old_controller.amm(), name="AMM", uri="https://api.arbiscan.io/api", api_key=ARBISCAN_API_KEY) - name = factory.names(ix) + '2' + old_controller = boa.from_etherscan( + factory.controllers(ix), + name="Controller", + uri="https://api.arbiscan.io/api", + api_key=ARBISCAN_API_KEY, + ) + old_amm = boa.from_etherscan( + old_controller.amm(), + name="AMM", + uri="https://api.arbiscan.io/api", + api_key=ARBISCAN_API_KEY, + ) + name = factory.names(ix) + "2" vault = factory.create( old_controller.borrowed_token(), old_controller.collateral_token(), @@ -67,7 +86,7 @@ def account_load(fname): old_amm.price_oracle_contract(), name, int(MINMAX[ix][0] / 365 / 86400), - int(MINMAX[ix][1] / 365 / 86400) + int(MINMAX[ix][1] / 365 / 86400), ) salt = os.urandom(32) @@ -76,21 +95,23 @@ def account_load(fname): salts[ix] = salt print(name) - print(f'Vault: {vault}') - print(f'Salt: {salt.hex()}') - print(f'Gauge: {gauge}') + print(f"Vault: {vault}") + print(f"Salt: {salt.hex()}") + print(f"Gauge: {gauge}") print() - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) boa.env.add_account(babe_raw) boa.env._fork_try_prefetch_state = False - gauge_factory_eth = boa.from_etherscan(gauge_factory.address, name="GaugeFactoryETH", api_key=ETHERSCAN_API_KEY) + gauge_factory_eth = boa.from_etherscan( + gauge_factory.address, name="GaugeFactoryETH", api_key=ETHERSCAN_API_KEY + ) for i in INDEXES: - print(f'Deploying a root gauge: {gauges[i]}') + print(f"Deploying a root gauge: {gauges[i]}") gauge_factory_eth.deploy_gauge(CHAIN_ID, salts[i]) diff --git a/scripts/recreate-eth-and-wsteth.py b/scripts/recreate-eth-and-wsteth.py index b5fcbb6e..b3faa73a 100755 --- a/scripts/recreate-eth-and-wsteth.py +++ b/scripts/recreate-eth-and-wsteth.py @@ -48,23 +48,25 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(NETWORK) - boa.env.add_account(account_load('babe')) + boa.env.add_account(account_load("babe")) boa.env._fork_try_prefetch_state = False - factory = boa.load_partial('contracts/lending/OneWayLendingFactory.vy').at(FACTORY) - policy_deployer = boa.load_partial('contracts/mpolicies/SecondaryMonetaryPolicy.vy') + factory = boa.load_partial("contracts/lending/OneWayLendingFactory.vy").at(FACTORY) + policy_deployer = boa.load_partial("contracts/mpolicies/SecondaryMonetaryPolicy.vy") # for ix in [WETH_INDEX, WSTETH_INDEX]: for ix in [WSTETH_INDEX]: @@ -72,9 +74,13 @@ def account_load(fname): rate_shift = int(0.03e18 / 365 / 86400) else: rate_shift = 0 - old_controller = boa.from_etherscan(factory.controllers(ix), name="Controller", api_key=ETHERSCAN_API_KEY) - old_amm = boa.from_etherscan(old_controller.amm(), name="AMM", api_key=ETHERSCAN_API_KEY) - name = factory.names(ix) + '2' + old_controller = boa.from_etherscan( + factory.controllers(ix), name="Controller", api_key=ETHERSCAN_API_KEY + ) + old_amm = boa.from_etherscan( + old_controller.amm(), name="AMM", api_key=ETHERSCAN_API_KEY + ) + name = factory.names(ix) + "2" vault = factory.create( old_controller.borrowed_token(), old_controller.collateral_token(), @@ -85,14 +91,21 @@ def account_load(fname): old_amm.price_oracle_contract(), name, int(0.03e18 / 365 / 86400), - int(0.3e18 / 365 / 86400) + rate_shift + int(0.3e18 / 365 / 86400) + rate_shift, + ) + policy = policy_deployer.deploy( + factory.address, + WETH_AMM, + old_controller.borrowed_token(), + target_utilization, + low_ratio, + high_ratio, + rate_shift, ) - policy = policy_deployer.deploy(factory.address, WETH_AMM, old_controller.borrowed_token(), - target_utilization, low_ratio, high_ratio, rate_shift) gauge = factory.deploy_gauge(vault) print(name) - print(f'Vault: {vault}') - print(f'Gauge: {gauge}') - print(f'Policy: {policy.address}') + print(f"Vault: {vault}") + print(f"Gauge: {gauge}") + print(f"Policy: {policy.address}") print() diff --git a/scripts/setup-metaregistry.py b/scripts/setup-metaregistry.py index fa6f58a5..8a08de62 100644 --- a/scripts/setup-metaregistry.py +++ b/scripts/setup-metaregistry.py @@ -1,11 +1,13 @@ from ape import project, accounts, Contract, networks from ape.cli import NetworkBoundCommand, network_option, account_option from ape.logging import logger + # account_option could be used when in prod? import click from dotenv import load_dotenv from pathlib import Path + BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) @@ -23,26 +25,23 @@ def _get_deployment_kw(network, account): - kw = {} - if not 'ethereum:' in network: + if "ethereum:" not in network: return is_sim = "ethereum:mainnet-fork" in network - + if not is_sim: - max_base_fee = networks.active_provider.base_fee * 2 kw = { - 'max_fee': max_base_fee, - 'max_priority_fee': min(int(0.5e9), max_base_fee) + "max_fee": max_base_fee, + "max_priority_fee": min(int(0.5e9), max_base_fee), } else: - - account = accounts["0xbabe61887f1de2713c6f97e567623453d3C79f67"] - + account = accounts["0xbabe61887f1de2713c6f97e567623453d3C79f67"] + return account, kw @@ -51,38 +50,35 @@ def cli(): """ Script for production deployment of crvUSD """ - - + + @cli.command(cls=NetworkBoundCommand) @network_option() @account_option() def clean(network, account): - account, kw = _get_deployment_kw(network=network, account=account) - + address_provider = Contract(ADDRESS_PROVIDER) address_provider_admin = Contract(address_provider.admin()) - + with accounts.use_sender(account) as account: - # unset addresses: for registry_id in [9, 10]: # we want to remove 9 and 10. 8 is still reserved. - if address_provider.get_address(registry_id) == ZERO_ADDRESS: continue - + address_provider_admin.execute( address_provider, address_provider.unset_address.encode_input( registry_id, ), - **kw + **kw, ) - + # sanity check: - assert address_provider.get_address(registry_id) == ZERO_ADDRESS + assert address_provider.get_address(registry_id) == ZERO_ADDRESS logger.info(f"Cleaned AddressProvider Registry ID: {registry_id}") - + # more sanity checks: for registry_id in range(9): assert address_provider.get_address(registry_id) != ZERO_ADDRESS @@ -92,28 +88,30 @@ def clean(network, account): @network_option() @account_option() def setup(network, account): - account, kw = _get_deployment_kw(network=network, account=account) - + if not STABLESWAP_FACTORY or not STABLECOIN: logger.error("Addresses for stableswap factory and stablecoin not set.") raise - + # ----------------- write to the chain ----------------- - + with accounts.use_sender(account) as account: - # -------------------- ADDRESSPROVIDER INTEGRATION ------------------------- - + address_provider = Contract(ADDRESS_PROVIDER) address_provider_admin = Contract(address_provider.admin()) - - occupied_slot = address_provider.get_address(STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) + + occupied_slot = address_provider.get_address( + STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID + ) if occupied_slot == ZERO_ADDRESS: # we should not be in this branch, since slot is already registered for factory logger.error("Empty slot for crvusd plain pools factory") - - logger.info(f"Registry at AddressProvider Registry ID: {STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID}: {occupied_slot}") + + logger.info( + f"Registry at AddressProvider Registry ID: {STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID}: {occupied_slot}" + ) logger.info("Updating to new registry ...") # update existing address provider id with new stableswap factory: @@ -125,51 +123,59 @@ def setup(network, account): ), **kw, ) - assert address_provider.get_address(STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) == STABLESWAP_FACTORY - logger.info(f"Updated AddressProvider Registry ID: {STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID} with {STABLESWAP_FACTORY}") - + assert ( + address_provider.get_address(STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) + == STABLESWAP_FACTORY + ) + logger.info( + f"Updated AddressProvider Registry ID: {STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID} with {STABLESWAP_FACTORY}" + ) + # -------------------- METAREGISTRY INTEGRATION ------------------------- - + # deploy factory handler: logger.info("Deploying new factory handler for stableswap factory ...") factory_handler = account.deploy( - project.StableswapFactoryHandler, STABLESWAP_FACTORY, BASE_POOL_REGISTRY, **kw + project.StableswapFactoryHandler, + STABLESWAP_FACTORY, + BASE_POOL_REGISTRY, + **kw, ) - + # integrate into metaregistry: metaregistry = Contract("0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC") previous_factory_handler = metaregistry.find_pool_for_coins(STABLECOIN, USDP, 0) factory_handler_integrated = previous_factory_handler != ZERO_ADDRESS - + if not factory_handler_integrated: - - logger.info("First integration ... adding factory handler to the metaregistry") - + logger.info( + "First integration ... adding factory handler to the metaregistry" + ) + # first integration into metaregistry: address_provider_admin.execute( metaregistry.address, metaregistry.add_registry_handler.encode_input(factory_handler), **kw, ) - + else: # redeployment, which means update handler index in metaregistry. - logger.info("Redeployment ... updating factory handler to the metaregistry") - + # get index of previous factory handler first: for idx in range(1000): if metaregistry.get_registry(idx) == previous_factory_handler: break - + # update that idx with newly deployed factory handler: address_provider_admin.execute( metaregistry.address, metaregistry.update_registry_handler.encode_input( idx, factory_handler.address ), - **kw + **kw, ) - + assert metaregistry.get_registry(idx) == factory_handler.address # sanity check: diff --git a/scripts/vote_new_arbi_impl.py b/scripts/vote_new_arbi_impl.py index 6eecb24b..26878464 100644 --- a/scripts/vote_new_arbi_impl.py +++ b/scripts/vote_new_arbi_impl.py @@ -17,17 +17,24 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': +if __name__ == "__main__": boa.env.fork(networks.ARBITRUM) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" - factory = boa.from_etherscan(FACTORY, name="L2Factory", uri="https://api.arbiscan.io/api", api_key=networks.ARBISCAN_API_KEY) + factory = boa.from_etherscan( + FACTORY, + name="L2Factory", + uri="https://api.arbiscan.io/api", + api_key=networks.ARBISCAN_API_KEY, + ) controller_impl = factory.controller_impl() amm_impl = factory.amm_impl() @@ -36,11 +43,11 @@ def account_load(fname): monetary_policy_impl = factory.monetary_policy_impl() gauge_factory = factory.gauge_factory() - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: boa.env.fork(networks.NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(networks.NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False @@ -51,32 +58,35 @@ def account_load(fname): vault_impl, pool_price_oracle_impl, monetary_policy_impl, - gauge_factory + gauge_factory, ) arbi_actions = [ (factory.address, factory_calldata), ] - actions = [ - (BROADCASTER, 'broadcast', arbi_actions, 10_000_000, 10**9) - ] + actions = [(BROADCASTER, "broadcast", arbi_actions, 10_000_000, 10**9)] vote_id = curve_dao.create_vote( target, actions, "Update Controller implementation for new LlamaLend markets on Arbitrum to support advanced leverage", networks.ETHERSCAN_API_KEY, - networks.PINATA_TOKEN + networks.PINATA_TOKEN, ) print(vote_id) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: # Simulating the vote - assert curve_dao.simulate(vote_id, target['voting'], networks.ETHERSCAN_API_KEY) + assert curve_dao.simulate(vote_id, target["voting"], networks.ETHERSCAN_API_KEY) # Simulating the Arbitrum side boa.env.fork(networks.ARBITRUM) boa.env.eoa = BROADCASTER - agent = boa.from_etherscan(ARBI_AGENT, name="ArbiOwnershipAgent", uri="https://api.arbiscan.io/api", api_key=networks.ARBISCAN_API_KEY) + agent = boa.from_etherscan( + ARBI_AGENT, + name="ArbiOwnershipAgent", + uri="https://api.arbiscan.io/api", + api_key=networks.ARBISCAN_API_KEY, + ) agent.execute(arbi_actions) assert CONTROLLER_IMPL == factory.controller_impl() diff --git a/scripts/vote_susde_mp.py b/scripts/vote_susde_mp.py index 7477a8d0..07935e2f 100644 --- a/scripts/vote_susde_mp.py +++ b/scripts/vote_susde_mp.py @@ -14,36 +14,36 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(networks.NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(networks.NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - controller_impl = boa.load_partial('contracts/Controller.vy') + controller_impl = boa.load_partial("contracts/Controller.vy") - actions = [ - (controller_impl.at(CONTROLLER), 'set_monetary_policy', MPOLICY) - ] + actions = [(controller_impl.at(CONTROLLER), "set_monetary_policy", MPOLICY)] vote_id = curve_dao.create_vote( target, actions, "Set monetary policy for the new sUSDe LlamaLend market, accourding to [https://gov.curve.fi/t/susde-llamalend-market-for-up-to-35x-leverage-with-a-special-monetary-policy/10132/]", networks.ETHERSCAN_API_KEY, - networks.PINATA_TOKEN + networks.PINATA_TOKEN, ) print(vote_id) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: # Simulating the vote - assert curve_dao.simulate(vote_id, target['voting'], networks.ETHERSCAN_API_KEY) + assert curve_dao.simulate(vote_id, target["voting"], networks.ETHERSCAN_API_KEY) diff --git a/scripts/vote_susde_params.py b/scripts/vote_susde_params.py index 439cb353..6afd310b 100644 --- a/scripts/vote_susde_params.py +++ b/scripts/vote_susde_params.py @@ -13,23 +13,25 @@ def account_load(fname): - path = os.path.expanduser(os.path.join('~', '.brownie', 'accounts', fname + '.json')) - with open(path, 'r') as f: + path = os.path.expanduser( + os.path.join("~", ".brownie", "accounts", fname + ".json") + ) + with open(path, "r") as f: pkey = account.decode_keyfile_json(json.load(f), getpass()) return account.Account.from_key(pkey) -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(networks.NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: - babe = account_load('babe') + babe = account_load("babe") boa.set_network_env(networks.NETWORK) boa.env.add_account(babe) boa.env._fork_try_prefetch_state = False - mpolicy = boa.load_partial('contracts/mpolicies/SusdeMonetaryPolicy.vy').at(MPOLICY) + mpolicy = boa.load_partial("contracts/mpolicies/SusdeMonetaryPolicy.vy").at(MPOLICY) target_util = int(0.8e18) low_ratio = int(0.35e18) @@ -37,18 +39,30 @@ def account_load(fname): rate_shift = 0 actions = [ - (mpolicy.at(MPOLICY), 'set_parameters', target_util, low_ratio, high_ratio, rate_shift) + ( + mpolicy.at(MPOLICY), + "set_parameters", + target_util, + low_ratio, + high_ratio, + rate_shift, + ) ] vote_id = curve_dao.create_vote( target, actions, "Update monetary policy for the new sUSDe LlamaLend market, according to [https://gov.curve.fi/t/change-rate-curve-for-susde-market-on-llamalend/10144]", networks.ETHERSCAN_API_KEY, - networks.PINATA_TOKEN + networks.PINATA_TOKEN, ) print(vote_id) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: # Simulating the vote - assert curve_dao.simulate(vote_id, target['voting'], networks.ETHERSCAN_API_KEY) - print(mpolicy.rate("0xB536FEa3a01c95Dd09932440eC802A75410139D6") * 365 * 86400 / 1e18) + assert curve_dao.simulate(vote_id, target["voting"], networks.ETHERSCAN_API_KEY) + print( + mpolicy.rate("0xB536FEa3a01c95Dd09932440eC802A75410139D6") + * 365 + * 86400 + / 1e18 + ) diff --git a/scripts/vote_weth_mp.py b/scripts/vote_weth_mp.py index b18ed946..634cee70 100644 --- a/scripts/vote_weth_mp.py +++ b/scripts/vote_weth_mp.py @@ -10,28 +10,26 @@ MPOLICY = "0x627bB157eBc0B77aD9F990DD2aD75878603abf08" -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(networks.NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(networks.NETWORK) babe = boa.env.add_accounts_from_rpc("http://localhost:1248") - controller_impl = boa.load_partial('contracts/Controller.vy') + controller_impl = boa.load_partial("contracts/Controller.vy") - actions = [ - (controller_impl.at(CONTROLLER), 'set_monetary_policy', MPOLICY) - ] + actions = [(controller_impl.at(CONTROLLER), "set_monetary_policy", MPOLICY)] vote_id = curve_dao.create_vote( target, actions, f"Set secondary monetary policy for the new {TOKEN} LlamaLend market", networks.ETHERSCAN_API_KEY, - networks.PINATA_TOKEN + networks.PINATA_TOKEN, ) print(vote_id) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: # Simulating the vote - assert curve_dao.simulate(vote_id, target['voting'], networks.ETHERSCAN_API_KEY) + assert curve_dao.simulate(vote_id, target["voting"], networks.ETHERSCAN_API_KEY) diff --git a/scripts/vote_wsteth_mp.py b/scripts/vote_wsteth_mp.py index fc9568a2..88cf0a85 100644 --- a/scripts/vote_wsteth_mp.py +++ b/scripts/vote_wsteth_mp.py @@ -10,28 +10,26 @@ MPOLICY = "0xbc7507bEA8d7bcb49f511cf59651B5114e6E7667" -if __name__ == '__main__': - if '--fork' in sys.argv[1:]: +if __name__ == "__main__": + if "--fork" in sys.argv[1:]: boa.env.fork(networks.NETWORK) - boa.env.eoa = '0xbabe61887f1de2713c6f97e567623453d3C79f67' + boa.env.eoa = "0xbabe61887f1de2713c6f97e567623453d3C79f67" else: boa.set_network_env(networks.NETWORK) babe = boa.env.add_accounts_from_rpc("http://localhost:1248") - controller_impl = boa.load_partial('contracts/Controller.vy') + controller_impl = boa.load_partial("contracts/Controller.vy") - actions = [ - (controller_impl.at(CONTROLLER), 'set_monetary_policy', MPOLICY) - ] + actions = [(controller_impl.at(CONTROLLER), "set_monetary_policy", MPOLICY)] vote_id = curve_dao.create_vote( target, actions, f"Set secondary monetary policy for the new {TOKEN} LlamaLend market", networks.ETHERSCAN_API_KEY, - networks.PINATA_TOKEN + networks.PINATA_TOKEN, ) print(vote_id) - if '--fork' in sys.argv[1:]: + if "--fork" in sys.argv[1:]: # Simulating the vote - assert curve_dao.simulate(vote_id, target['voting'], networks.ETHERSCAN_API_KEY) + assert curve_dao.simulate(vote_id, target["voting"], networks.ETHERSCAN_API_KEY) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..230782be --- /dev/null +++ b/tests/README.md @@ -0,0 +1,5 @@ +# Llamalend Tests + +If you think the folder layout is inconsistent, that's because it is! + +We're in the progress of migrating tests from a semantic layout (controller, flashloan, lending, mpolicy, vault) to a more structured layout (unit, fork, integration, fuzzing, stateful, etc). Thank you for your patience. diff --git a/tests/amm/conftest.py b/tests/amm/conftest.py index 647912a0..e63cfe81 100644 --- a/tests/amm/conftest.py +++ b/tests/amm/conftest.py @@ -1,34 +1,43 @@ import boa import pytest from math import sqrt, log +from tests.utils.deployers import AMM_DEPLOYER, ERC20_MOCK_DEPLOYER PRICE = 3000 -@pytest.fixture(scope="session") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(6) +@pytest.fixture(scope="module") +def borrowed_token(): + return ERC20_MOCK_DEPLOYER.deploy(6) -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def get_amm(price_oracle, admin, accounts): def f(collateral_token, borrowed_token): with boa.env.prank(admin): - amm = boa.load('contracts/AMM.vy', - borrowed_token.address, 10**(18 - borrowed_token.decimals()), - collateral_token.address, 10**(18 - collateral_token.decimals()), - 100, int(sqrt(100/99) * 1e18), int(log(100/99) * 1e18), - PRICE * 10**18, 10**16, 0, - price_oracle.address) + amm = AMM_DEPLOYER.deploy( + borrowed_token.address, + 10 ** (18 - borrowed_token.decimals()), + collateral_token.address, + 10 ** (18 - collateral_token.decimals()), + 100, + int(sqrt(100 / 99) * 1e18), + int(log(100 / 99) * 1e18), + PRICE * 10**18, + 10**16, + 0, + price_oracle.address, + ) amm.set_admin(admin) for acct in accounts: with boa.env.prank(acct): - collateral_token.approve(amm.address, 2**256-1) - borrowed_token.approve(amm.address, 2**256-1) + collateral_token.approve(amm.address, 2**256 - 1) + borrowed_token.approve(amm.address, 2**256 - 1) return amm + return f -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def amm(collateral_token, borrowed_token, get_amm): return get_amm(collateral_token, borrowed_token) diff --git a/tests/amm/test_amount_for_price.py b/tests/amm/test_amount_for_price.py index 1d34e2f5..9440c103 100644 --- a/tests/amm/test_amount_for_price.py +++ b/tests/amm/test_amount_for_price.py @@ -1,7 +1,8 @@ import boa -from ..conftest import approx +import pytest from hypothesis import given, settings from hypothesis import strategies as st +from ..utils import mint_for_testing @given( @@ -10,11 +11,23 @@ dn=st.integers(min_value=0, max_value=49), deposit_amount=st.integers(min_value=10**12, max_value=10**20), init_trade_frac=st.floats(min_value=0.0, max_value=1.0), - p_frac=st.floats(min_value=0.1, max_value=10) + p_frac=st.floats(min_value=0.1, max_value=10), ) @settings(max_examples=5000) -def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowed_token, admin, - oracle_price, n1, dn, deposit_amount, init_trade_frac, p_frac): +def test_amount_for_price( + price_oracle, + amm, + accounts, + collateral_token, + borrowed_token, + admin, + oracle_price, + n1, + dn, + deposit_amount, + init_trade_frac, + p_frac, +): user = accounts[0] with boa.env.prank(admin): amm.set_fee(0) @@ -25,17 +38,15 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe # Initial deposit with boa.env.prank(admin): amm.deposit_range(user, deposit_amount, n1, n2) - collateral_token._mint_for_testing(amm.address, deposit_amount) - - prices = [oracle_price] - prices.append(amm.get_p()) + mint_for_testing(collateral_token, amm.address, deposit_amount) with boa.env.prank(user): # Dump some to be somewhere inside the bands eamount = int(deposit_amount * amm.get_p() // 10**18 * init_trade_frac) if eamount > 0: - borrowed_token._mint_for_testing(user, eamount) + mint_for_testing(borrowed_token, user, eamount) boa.env.time_travel(600) # To reset the prev p_o counter + amm.exchange(0, 1, eamount, 0) n0 = amm.active_band() @@ -49,61 +60,68 @@ def test_amount_for_price(price_oracle, amm, accounts, collateral_token, borrowe assert is_pump == (p_final >= p_initial) if is_pump: - borrowed_token._mint_for_testing(user, amount) + mint_for_testing(borrowed_token, user, amount) amm.exchange(0, 1, amount, 0) else: - collateral_token._mint_for_testing(user, amount) + mint_for_testing(collateral_token, user, amount) amm.exchange(1, 0, amount, 0) p = amm.get_p() - prices.append(p) - - prec = 1e-6 - if amount > 0: - if is_pump: - prec += 2 / amount + 2 / (1e12 * amount * 1e18 / p_max) - else: - prec += 2 / amount + 2 / (amount * p_max / 1e18 / 1e12) - else: - return - prec = max(prec, 1e-3) - n_final = amm.active_band() - assert approx(p_max, amm.p_current_up(n2), 1e-8) - assert approx(p_min, amm.p_current_down(n1), 1e-8) + if eamount > 0: + assert abs(n_final - n0) < 50 - if abs(n_final - n0) < 50 - 1: + if p_final > p_max: + p_final = p_max + if p_final < p_min: + p_final = p_min + + if p == pytest.approx(p_final, rel=1e-3): + assert n1 <= n_final <= n2 + else: A = amm.A() a_ratio = A / (A - 1) - p_o_ratio = amm.p_oracle_up(n_final) / amm.price_oracle() if is_pump: - if p_o_ratio < a_ratio**-50 * (1 + 1e-8): - return + if p_min == p_final: + # The AMM gives the wrong price based on n0 band. It should be DUMP, not PUMP + assert n_final == n0 and amount == 0 + else: + # Nothing to pump for OR too far to pump + _n_final = max(n_final, n1) + p_o_ratio = amm.p_oracle_up(_n_final) / oracle_price + assert collateral_token.balanceOf( + amm + ) == 0 or p_o_ratio < a_ratio**-50 * (1 + 1e-8) else: - if p_o_ratio > a_ratio**50 * (1 - 1e-8): - return - - if p_final > p_min * (1 + prec) and p_final < p_max * (1 - prec): - assert approx(p, p_final, prec) - - elif p_final >= p_max * (1 - prec): - if not approx(p, p_max, prec): - assert n_final > n2 - - elif p_final <= p_min * (1 + prec): - if not approx(p, p_min, prec): - assert n_final < n1 - - -def test_amount_for_price_ticks_too_far(price_oracle, amm, accounts, collateral_token, borrowed_token, admin): + if p_max == p_final: + # The AMM gives the wrong price based on n0 band. It should be PUMP, not DUMP + assert n_final == n0 and amount == 0 + else: + # Nothing to dump for OR too far to dump + _n_final = min(n_final, n2) + p_o_ratio = amm.p_oracle_up(_n_final) / oracle_price + assert borrowed_token.balanceOf(amm) == 0 or p_o_ratio > a_ratio**50 * ( + 1 - 1e-8 + ) + + +def test_amount_for_price_ticks_too_far( + price_oracle, amm, accounts, collateral_token, borrowed_token, admin +): with boa.env.anchor(): test_amount_for_price.hypothesis.inner_test( - price_oracle, amm, accounts, collateral_token, borrowed_token, admin, + price_oracle, + amm, + accounts, + collateral_token, + borrowed_token, + admin, oracle_price=2000000000000000000000, n1=50, dn=0, deposit_amount=1000000000000, init_trade_frac=0.0, - p_frac=2891564947520759727/1e18) + p_frac=2891564947520759727 / 1e18, + ) diff --git a/tests/amm/test_deposit_withdraw.py b/tests/amm/test_deposit_withdraw.py index bd30f7ca..0c61e4be 100644 --- a/tests/amm/test_deposit_withdraw.py +++ b/tests/amm/test_deposit_withdraw.py @@ -1,19 +1,24 @@ import boa -from ..conftest import approx +import pytest from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing -DEAD_SHARES = 10**3 +from tests.utils.constants import DEAD_SHARES @given( - amounts=st.lists(st.integers(min_value=0, max_value=10**6 * 10**18), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=-20, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), - fracs=st.lists(st.integers(min_value=0, max_value=10**18), min_size=5, max_size=5) + amounts=st.lists( + st.integers(min_value=0, max_value=10**6 * 10**18), min_size=5, max_size=5 + ), + ns=st.lists(st.integers(min_value=-20, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + fracs=st.lists(st.integers(min_value=0, max_value=10**18), min_size=5, max_size=5), ) -def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_token, admin): +def test_deposit_withdraw( + amm, amounts, accounts, ns, dns, fracs, collateral_token, admin +): deposits = {} precisions = {} with boa.env.prank(admin): @@ -24,20 +29,24 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok precisions[user] = DEAD_SHARES / (amount // (dn + 1)) + 1e-6 n2 = n1 + dn if amount // (dn + 1) <= 100: - with boa.reverts('Amount too low'): + with boa.reverts("Amount too low"): amm.deposit_range(user, amount, n1, n2) else: amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) deposits[user] = amount assert collateral_token.balanceOf(user) == 0 for user, n1 in zip(accounts, ns): if user in deposits: if n1 >= 0: - assert approx(amm.get_y_up(user), deposits[user], precisions[user], 25) + assert amm.get_y_up(user) == pytest.approx( + deposits[user], rel=precisions[user], abs=25 + ) else: - assert amm.get_y_up(user) < deposits[user] # price manipulation caused loss for user + assert ( + amm.get_y_up(user) < deposits[user] + ) # price manipulation caused loss for user else: assert amm.get_y_up(user) == 0 @@ -46,7 +55,11 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok before = amm.get_sum_xy(user) amm.withdraw(user, frac) after = amm.get_sum_xy(user) - assert approx(before[1] - after[1], deposits[user] * frac / 1e18, precisions[user], 25 + deposits[user] * precisions[user]) + assert before[1] - after[1] == pytest.approx( + deposits[user] * frac / 1e18, + rel=precisions[user], + abs=25 + deposits[user] * precisions[user], + ) else: with boa.reverts("No deposits"): amm.withdraw(user, frac) @@ -54,4 +67,13 @@ def test_deposit_withdraw(amm, amounts, accounts, ns, dns, fracs, collateral_tok def test_deposit_withdraw_1(amm, accounts, collateral_token, admin): with boa.env.anchor(): - test_deposit_withdraw.hypothesis.inner_test(amm, [10**6]+[0]*4, accounts, [0]*5, [0]*5, [10**18]*5, collateral_token, admin) + test_deposit_withdraw.hypothesis.inner_test( + amm, + [10**6] + [0] * 4, + accounts, + [0] * 5, + [0] * 5, + [10**18] * 5, + collateral_token, + admin, + ) diff --git a/tests/amm/test_exchange.py b/tests/amm/test_exchange.py index 229b1e79..5780dda4 100644 --- a/tests/amm/test_exchange.py +++ b/tests/amm/test_exchange.py @@ -1,20 +1,23 @@ import boa -from ..conftest import approx +import pytest from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing @given( - amounts=st.lists(st.integers(min_value=10**16, max_value=10**6 * 10**18), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + amounts=st.lists( + st.integers(min_value=10**16, max_value=10**6 * 10**18), min_size=5, max_size=5 + ), + ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), ) def test_dxdy_limits(amm, amounts, accounts, ns, dns, collateral_token, admin): with boa.env.prank(admin): for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dxdy(0, 1, 0) @@ -26,41 +29,48 @@ def test_dxdy_limits(amm, amounts, accounts, ns, dns, collateral_token, admin): dx, dy = amm.get_dxdy(0, 1, 10**2) # $0.0001 assert dx == 10**2 if min(ns) == 1: - assert approx(dy, dx * 10**(18 - 6) / 3000, 4e-2 + 2 * min(ns) / amm.A()) + assert dy == pytest.approx( + dx * 10 ** (18 - 6) / 3000, rel=4e-2 + 2 * min(ns) / amm.A() + ) else: - assert dy <= dx * 10**(18 - 6) / 3000 + assert dy <= dx * 10 ** (18 - 6) / 3000 dx, dy = amm.get_dxdy(1, 0, 10**16) # No liquidity assert dx == 0 assert dy == 0 # Rounded down # Huge swap dx, dy = amm.get_dxdy(0, 1, 10**12 * 10**6) - assert dx < 10**12 * 10**6 # Less than all is spent - assert abs(dy - sum(amounts)) <= 1000 # but everything is bought + assert dx < 10**12 * 10**6 # Less than all is spent + assert abs(dy - sum(amounts)) <= 1000 # but everything is bought dx, dy = amm.get_dxdy(1, 0, 10**12 * 10**18) assert dx == 0 assert dy == 0 # Rounded down @given( - amounts=st.lists(st.integers(min_value=10**16, max_value=10**6 * 10**18), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), - amount=st.integers(min_value=0, max_value=10**9 * 10**6) + amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), + ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + amount=st.floats(min_value=0.001, max_value=10e9), ) -def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, - borrowed_token, collateral_token, admin): +def test_exchange_down_up( + amm, amounts, accounts, ns, dns, amount, borrowed_token, collateral_token, admin +): + collateral_decimals = collateral_token.decimals() + borrowed_decimals = borrowed_token.decimals() + amounts = list(map(lambda x: int(x * 10**collateral_decimals), amounts)) + amount = int(amount * 10**borrowed_decimals) u = accounts[6] with boa.env.prank(admin): - for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): + for user, amt, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn - if amount // (dn + 1) <= 100: + if amt // (dn + 1) <= 100: with boa.reverts("Amount too low"): - amm.deposit_range(user, amount, n1, n2) + amm.deposit_range(user, amt, n1, n2) else: - amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + amm.deposit_range(user, amt, n1, n2) + mint_for_testing(collateral_token, amm.address, amt) p_before = amm.get_p() @@ -68,8 +78,8 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, assert dx <= amount dx2, dy2 = amm.get_dxdy(0, 1, dx) assert dx == dx2 - assert approx(dy, dy2, 1e-6) - borrowed_token._mint_for_testing(u, dx2) + assert dy == pytest.approx(dy2, rel=1e-6) + mint_for_testing(borrowed_token, u, dx2) with boa.env.prank(u): amm.exchange(0, 1, dx2, 0) assert borrowed_token.balanceOf(u) == 0 @@ -80,18 +90,20 @@ def test_exchange_down_up(amm, amounts, accounts, ns, dns, amount, sum_borrowed = sum(amm.bands_x(i) for i in range(50)) sum_collateral = sum(amm.bands_y(i) for i in range(50)) - assert abs(borrowed_token.balanceOf(amm) - sum_borrowed // 10**(18 - 6)) <= 1 + assert abs(borrowed_token.balanceOf(amm) - sum_borrowed // 10 ** (18 - 6)) <= 1 assert abs(collateral_token.balanceOf(amm) - sum_collateral) <= 1 in_amount = int(dy2 / 0.98) # two trades charge 1% twice expected_out_amount = dx2 dx, dy = amm.get_dxdy(1, 0, in_amount) - assert approx(dx, in_amount, 5e-4) # Not precise because fee is charged on different directions + assert dx == pytest.approx( + in_amount, rel=5e-4 + ) # Not precise because fee is charged on different directions assert dy <= expected_out_amount assert abs(dy - expected_out_amount) <= 2 * fee * expected_out_amount - collateral_token._mint_for_testing(u, dx - collateral_token.balanceOf(u)) + mint_for_testing(collateral_token, u, dx - collateral_token.balanceOf(u)) dy_measured = borrowed_token.balanceOf(u) dx_measured = collateral_token.balanceOf(u) with boa.env.prank(u): diff --git a/tests/amm/test_exchange_dy.py b/tests/amm/test_exchange_dy.py index f7431bfc..0f4d5a3f 100644 --- a/tests/amm/test_exchange_dy.py +++ b/tests/amm/test_exchange_dy.py @@ -1,13 +1,15 @@ import boa import pytest -from ..conftest import approx from hypothesis import given from hypothesis import strategies as st +from ..utils import mint_for_testing +from tests.utils.deployers import ERC20_MOCK_DEPLOYER @pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) +def borrowed_token(): + """Override borrowed_token to fix decimals at 18 for this module.""" + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") @@ -16,20 +18,22 @@ def amm(get_amm, borrowed_token, collateral_token): @given( - amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), + ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), ) -def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, borrowed_token): +def test_dydx_limits( + amm, amounts, accounts, ns, dns, collateral_token, admin, borrowed_token +): collateral_decimals = collateral_token.decimals() borrowed_decimals = borrowed_token.decimals() - amounts = list(map(lambda x: int(x * 10 ** collateral_decimals), amounts)) + amounts = list(map(lambda x: int(x * 10**collateral_decimals), amounts)) with boa.env.prank(admin): for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dx, dy = amm.get_dydx(0, 1, 0) @@ -38,13 +42,16 @@ def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, b assert dx == dy == 0 # Small swap - dy, dx = amm.get_dydx(0, 1, 10**(collateral_decimals - 6)) # 0.000001 ETH + dy, dx = amm.get_dydx(0, 1, 10 ** (collateral_decimals - 6)) # 0.000001 ETH assert dy == 10**12 if min(ns) == 1: - assert approx(dx, dy * 3000 / 10**(collateral_decimals - borrowed_decimals), 4e-2 + 2 * min(ns) / amm.A()) + assert dx == pytest.approx( + dy * 3000 / 10 ** (collateral_decimals - borrowed_decimals), + rel=4e-2 + 2 * min(ns) / amm.A(), + ) else: - assert dx >= dy * 3000 / 10**(collateral_decimals - borrowed_decimals) - dy, dx = amm.get_dydx(1, 0, 10**(borrowed_decimals - 4)) # No liquidity + assert dx >= dy * 3000 / 10 ** (collateral_decimals - borrowed_decimals) + dy, dx = amm.get_dydx(1, 0, 10 ** (borrowed_decimals - 4)) # No liquidity assert dx == 0 assert dy == 0 # Rounded down @@ -58,11 +65,13 @@ def test_dydx_limits(amm, amounts, accounts, ns, dns, collateral_token, admin, b @given( - amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), + ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), ) -def test_dydx_compare_to_dxdy(amm, amounts, accounts, ns, dns, collateral_token, admin, borrowed_token): +def test_dydx_compare_to_dxdy( + amm, amounts, accounts, ns, dns, collateral_token, admin, borrowed_token +): collateral_decimals = collateral_token.decimals() borrowed_decimals = borrowed_token.decimals() amounts = list(map(lambda x: int(x * 10**collateral_decimals), amounts)) @@ -77,7 +86,7 @@ def test_dydx_compare_to_dxdy(amm, amounts, accounts, ns, dns, collateral_token, for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + mint_for_testing(collateral_token, amm.address, amount) # Swap 0 dy, dx = amm.get_dydx(0, 1, 0) @@ -86,33 +95,33 @@ def test_dydx_compare_to_dxdy(amm, amounts, accounts, ns, dns, collateral_token, assert dx == dy == 0 # Small swap - dy1, dx1 = amm.get_dydx(0, 1, 10**(collateral_decimals - 2)) + dy1, dx1 = amm.get_dydx(0, 1, 10 ** (collateral_decimals - 2)) dx2, dy2 = amm.get_dxdy(0, 1, dx1) assert dx1 == dx2 assert abs(dy1 - dy2) <= collateral_precision - dx1, dy1 = amm.get_dxdy(0, 1, 10**(borrowed_decimals - 2)) + dx1, dy1 = amm.get_dxdy(0, 1, 10 ** (borrowed_decimals - 2)) dy2, dx2 = amm.get_dydx(0, 1, dy1) assert abs(dx1 - dx2) <= borrowed_precision assert dy1 == dy2 - dy, dx = amm.get_dydx(1, 0, 10**(collateral_decimals - 2)) # No liquidity + dy, dx = amm.get_dydx(1, 0, 10 ** (collateral_decimals - 2)) # No liquidity assert dx == 0 assert dy == 0 # Rounded down # Huge swap dy1, dx1 = amm.get_dydx(0, 1, 10**12 * 10**collateral_decimals) dx2, dy2 = amm.get_dxdy(0, 1, dx1) - assert dy1 < 10**12 * 10**collateral_decimals # Less than all is desired - assert abs(dy1 - sum(amounts)) <= 1000 # but everything is bought + assert dy1 < 10**12 * 10**collateral_decimals # Less than all is desired + assert abs(dy1 - sum(amounts)) <= 1000 # but everything is bought assert dx1 == dx2 assert dy2 <= dy1 # We might get less because AMM rounds in its favor assert abs(dy1 - dy2) <= collateral_precision dx1, dy1 = amm.get_dxdy(0, 1, 10**12 * 10**borrowed_decimals) dy2, dx2 = amm.get_dydx(0, 1, dy1) - assert dx1 < 10**12 * 10**borrowed_decimals # Less than all is spent - assert abs(dy1 - sum(amounts)) <= 1000 # but everything is bought + assert dx1 < 10**12 * 10**borrowed_decimals # Less than all is spent + assert abs(dy1 - sum(amounts)) <= 1000 # but everything is bought assert dx1 == dx2 assert dy1 == dy2 @@ -122,27 +131,29 @@ def test_dydx_compare_to_dxdy(amm, amounts, accounts, ns, dns, collateral_token, @given( - amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), - ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), - dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), - amount=st.floats(min_value=0, max_value=10e9) + amounts=st.lists(st.floats(min_value=0.01, max_value=1e6), min_size=5, max_size=5), + ns=st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5), + dns=st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5), + amount=st.floats(min_value=0.001, max_value=10e9), ) -def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_token, collateral_token, admin): +def test_exchange_dy_down_up( + amm, amounts, accounts, ns, dns, amount, borrowed_token, collateral_token, admin +): collateral_decimals = collateral_token.decimals() borrowed_decimals = borrowed_token.decimals() amounts = list(map(lambda x: int(x * 10**collateral_decimals), amounts)) - amount = amount * 10**borrowed_decimals + amount = int(amount * 10**borrowed_decimals) u = accounts[6] with boa.env.prank(admin): - for user, amount, n1, dn in zip(accounts[1:6], amounts, ns, dns): + for user, amt, n1, dn in zip(accounts[1:6], amounts, ns, dns): n2 = n1 + dn - if amount // (dn + 1) <= 100: + if amt // (dn + 1) <= 100: with boa.reverts("Amount too low"): - amm.deposit_range(user, amount, n1, n2) + amm.deposit_range(user, amt, n1, n2) else: - amm.deposit_range(user, amount, n1, n2) - collateral_token._mint_for_testing(amm.address, amount) + amm.deposit_range(user, amt, n1, n2) + mint_for_testing(collateral_token, amm.address, amt) p_before = amm.get_p() @@ -151,8 +162,8 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t assert dy <= amount dy2, dx2 = amm.get_dydx(0, 1, dy) assert dy == dy2 - assert approx(dx, dx2, 1e-6) - borrowed_token._mint_for_testing(u, dx2) + assert dx == pytest.approx(dx2, rel=1e-6) + mint_for_testing(borrowed_token, u, dx2) with boa.env.prank(u): with boa.reverts("Slippage"): amm.exchange_dy(0, 1, dy2, dx2 - 1) # crvUSD --> ETH @@ -165,8 +176,20 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t sum_borrowed = sum(amm.bands_x(i) for i in range(50)) sum_collateral = sum(amm.bands_y(i) for i in range(50)) - assert abs(borrowed_token.balanceOf(amm) - sum_borrowed // 10**(18 - borrowed_decimals)) <= 1 - assert abs(collateral_token.balanceOf(amm) - sum_collateral // 10**(18 - collateral_decimals)) <= 1 + assert ( + abs( + borrowed_token.balanceOf(amm) + - sum_borrowed // 10 ** (18 - borrowed_decimals) + ) + <= 1 + ) + assert ( + abs( + collateral_token.balanceOf(amm) + - sum_collateral // 10 ** (18 - collateral_decimals) + ) + <= 1 + ) # ETH --> crvUSD (dx - ETH, dy - crvUSD) expected_in_amount = dy2 @@ -177,7 +200,7 @@ def test_exchange_dy_down_up(amm, amounts, accounts, ns, dns, amount, borrowed_t assert abs(dx - expected_in_amount) <= 2 * (fee + 0.01) * expected_in_amount assert out_amount - dy <= 1 - collateral_token._mint_for_testing(u, dx - collateral_token.balanceOf(u)) + mint_for_testing(collateral_token, u, dx - collateral_token.balanceOf(u)) dy_measured = borrowed_token.balanceOf(u) dx_measured = collateral_token.balanceOf(u) with boa.env.prank(u): diff --git a/tests/amm/test_flip.py b/tests/amm/test_flip.py index f6b59e0b..3e3f4c1c 100644 --- a/tests/amm/test_flip.py +++ b/tests/amm/test_flip.py @@ -1,5 +1,6 @@ import boa from pytest import mark # noqa +from ..utils import mint_for_testing # 1. deposit below (N > 0 in 5 bands) # 2. change price_oracle in a cycle downwards (by 15% just in case?) @@ -19,7 +20,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm # We deposit to bands 1..5 with boa.env.prank(admin): amm.deposit_range(depositor, AMOUNT_D, 1, 5) - collateral_token._mint_for_testing(amm.address, AMOUNT_D) + mint_for_testing(collateral_token, amm.address, AMOUNT_D) p = amm.price_oracle() initial_y = sum(amm.bands_y(n) for n in range(1, 6)) @@ -33,13 +34,15 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm # which means that it becomes lower than p, and we need to buy until we have reached p # trade - dx = int(STEP * AMOUNT_D * p / 1e18 / 10**(18-6)) + dx = int(STEP * AMOUNT_D * p / 1e18 / 10 ** (18 - 6)) is_empty = False while amm.get_p() < p: - borrowed_token._mint_for_testing(trader, dx) + mint_for_testing(borrowed_token, trader, dx) n1 = amm.active_band() p1 = amm.get_p() - assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum(amm.bands_y(n) for n in range(1, 6)) + assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum( + amm.bands_y(n) for n in range(1, 6) + ) assert amm.get_x_down(depositor) * (1 + 1e-13) >= 5 * 0.95 * 3000 * 1e6 with boa.env.prank(trader): amm.exchange(0, 1, dx, 0) @@ -55,8 +58,8 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm if is_empty: break - converted_x = sum(amm.bands_x(n) for n in range(1, 6)) // 10**(18 - 6) - assert converted_x >= 5 * 0.95**0.5 * amm.p_oracle_down(1)/1e18 * 1e6 + converted_x = sum(amm.bands_x(n) for n in range(1, 6)) // 10 ** (18 - 6) + assert converted_x >= 5 * 0.95**0.5 * amm.p_oracle_down(1) / 1e18 * 1e6 # Sell until we have 0 coins left while True: @@ -64,10 +67,12 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm is_empty = False while amm.get_p() > p: if collateral_token.balanceOf(trader) < dy: - collateral_token._mint_for_testing(trader, dy) + mint_for_testing(collateral_token, trader, dy) n1 = amm.active_band() p1 = amm.get_p() - assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum(amm.bands_y(n) for n in range(1, 6)) + assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum( + amm.bands_y(n) for n in range(1, 6) + ) assert amm.get_x_down(depositor) * (1 + 1e-13) >= 5 * 0.95 * 3000 * 1e6 with boa.env.prank(trader): amm.exchange(1, 0, dy, 0) diff --git a/tests/amm/test_flip_dy.py b/tests/amm/test_flip_dy.py index ab6752b6..ed6c6655 100644 --- a/tests/amm/test_flip_dy.py +++ b/tests/amm/test_flip_dy.py @@ -1,6 +1,7 @@ import boa import pytest from pytest import mark # noqa +from ..utils import mint_for_testing # 1. deposit below (N > 0 in 5 bands) # 2. change price_oracle in a cycle downwards (by 15% just in case?) @@ -11,11 +12,6 @@ STEP = 0.01 -@pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) - - @pytest.fixture(scope="module") def amm(get_amm, borrowed_token, collateral_token): return get_amm(collateral_token, borrowed_token) @@ -33,7 +29,7 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm # We deposit to bands 1..5 with boa.env.prank(admin): amm.deposit_range(depositor, amount_d, 1, 5) - collateral_token._mint_for_testing(amm.address, amount_d) + mint_for_testing(collateral_token, amm.address, amount_d) p = amm.price_oracle() initial_y = sum(amm.bands_y(n) for n in range(1, 6)) @@ -47,17 +43,28 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm # which means that it becomes lower than p, and we need to buy until we have reached p # trade - dx = int(STEP * amount_d * p / 10**collateral_decimals / 10**(collateral_decimals - borrowed_decimals)) + dx = int( + STEP + * amount_d + * p + / 10**collateral_decimals + / 10 ** (collateral_decimals - borrowed_decimals) + ) # dy = int(STEP * amount_d) <-- this leads to LOSS is_empty = False while amm.get_p() < p: dy = amm.get_dy(0, 1, dx) dx = amm.get_dx(0, 1, dy) - borrowed_token._mint_for_testing(trader, dx) + mint_for_testing(borrowed_token, trader, dx) n1 = amm.active_band() p1 = amm.get_p() - assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum(amm.bands_y(n) for n in range(1, 6)) - assert amm.get_x_down(depositor) * (1 + 1e-13) >= 5 * 0.95 * 3000 * 10**borrowed_decimals + assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum( + amm.bands_y(n) for n in range(1, 6) + ) + assert ( + amm.get_x_down(depositor) * (1 + 1e-13) + >= 5 * 0.95 * 3000 * 10**borrowed_decimals + ) with boa.env.prank(trader): amm.exchange_dy(0, 1, dy, dx) n2 = amm.active_band() @@ -66,18 +73,36 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm assert p2 >= p1 assert p2 >= amm.p_current_down(n2) assert p2 <= amm.p_current_up(n2) - is_empty = sum(amm.bands_y(n) for n in range(1, 6)) == 0 + is_empty = sum(amm.bands_y(n) for n in range(1, 6)) < 10 ** ( + 18 - collateral_decimals + ) if is_empty: + assert collateral_token.balanceOf(amm) <= 1 break if is_empty: break - converted_x = sum(amm.bands_x(n) for n in range(1, 6)) // 10**(collateral_decimals - borrowed_decimals) - assert converted_x >= 5 * 0.95**0.5 * amm.p_oracle_down(1) / 10**collateral_decimals * 10**borrowed_decimals + converted_x = sum(amm.bands_x(n) for n in range(1, 6)) // 10 ** ( + collateral_decimals - borrowed_decimals + ) + assert ( + converted_x + >= 5 + * 0.95**0.5 + * amm.p_oracle_down(1) + / 10**collateral_decimals + * 10**borrowed_decimals + ) # Sell until we have 0 coins left while True: - dx = int(STEP * amount_d * p / 10**collateral_decimals / 10**(collateral_decimals - borrowed_decimals)) + dx = int( + STEP + * amount_d + * p + / 10**collateral_decimals + / 10 ** (collateral_decimals - borrowed_decimals) + ) # dy = int(STEP * amount_d) <-- this leads to INFINITE LOOP is_empty = False while amm.get_p() > p: @@ -88,11 +113,16 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm with boa.reverts(): amm.get_dx(1, 0, dx) if collateral_token.balanceOf(trader) < dy: - collateral_token._mint_for_testing(trader, dy) + mint_for_testing(collateral_token, trader, dy) n1 = amm.active_band() p1 = amm.get_p() - assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum(amm.bands_y(n) for n in range(1, 6)) - assert amm.get_x_down(depositor) * (1 + 1e-13) >= 5 * 0.95 * 3000 * 10**borrowed_decimals + assert amm.get_y_up(depositor) * (1 + 1e-13) >= sum( + amm.bands_y(n) for n in range(1, 6) + ) + assert ( + amm.get_x_down(depositor) * (1 + 1e-13) + >= 5 * 0.95 * 3000 * 10**borrowed_decimals + ) with boa.env.prank(trader): if dx == dx_small: amm.exchange_dy(1, 0, dx, dy) @@ -106,8 +136,11 @@ def test_flip(amm, price_oracle, collateral_token, borrowed_token, accounts, adm assert p2 <= p1 assert p2 >= amm.p_current_down(n2) assert p2 <= amm.p_current_up(n2) - is_empty = sum(amm.bands_x(n) for n in range(1, 6)) == 0 + is_empty = sum(amm.bands_x(n) for n in range(1, 6)) < 10 ** ( + 18 - borrowed_decimals + ) if is_empty: + assert borrowed_token.balanceOf(amm) <= 1 break if is_empty: diff --git a/tests/amm/test_oracle_change_noloss.py b/tests/amm/test_oracle_change_noloss.py index 5aed7361..f9a1c153 100644 --- a/tests/amm/test_oracle_change_noloss.py +++ b/tests/amm/test_oracle_change_noloss.py @@ -4,11 +4,7 @@ import pytest from hypothesis import given, settings, example from hypothesis import strategies as st - - -@pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) +from ..utils import mint_for_testing @pytest.fixture(scope="module") @@ -20,21 +16,31 @@ def amm(collateral_token, borrowed_token, get_amm): n1=st.integers(min_value=1, max_value=60), # Max is probably unreachable dn=st.integers(min_value=0, max_value=20), amount=st.integers(min_value=10**10, max_value=10**20), - price_shift=st.floats(min_value=0.9, max_value=1.1) + price_shift=st.floats(min_value=0.9, max_value=1.1), ) @settings(max_examples=1000) -def test_buy_with_shift(amm, collateral_token, borrowed_token, price_oracle, accounts, admin, - n1, dn, amount, price_shift): +def test_buy_with_shift( + amm, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, + n1, + dn, + amount, + price_shift, +): user = accounts[1] collateral_amount = 10**18 # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - collateral_token._mint_for_testing(amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + mint_for_testing(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -49,7 +55,7 @@ def test_buy_with_shift(amm, collateral_token, borrowed_token, price_oracle, acc price_oracle.set_price(int(price_oracle.price() * price_shift)) # Trade back - collateral_token._mint_for_testing(user, 10**24) # BIG + mint_for_testing(collateral_token, user, 10**24) # BIG with boa.env.prank(user): amm.exchange(1, 0, 10**24, 0) # Check that we cleaned up the last band @@ -65,11 +71,21 @@ def test_buy_with_shift(amm, collateral_token, borrowed_token, price_oracle, acc n1=st.integers(min_value=1, max_value=20), # Max is probably unreachable dn=st.integers(min_value=0, max_value=20), amount=st.integers(min_value=10**10, max_value=10**18), - price_shift=st.floats(min_value=0.1, max_value=10) + price_shift=st.floats(min_value=0.1, max_value=10), ) @settings(max_examples=1000) -def test_sell_with_shift(amm, collateral_token, borrowed_token, price_oracle, accounts, admin, - n1, dn, amount, price_shift): +def test_sell_with_shift( + amm, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, + n1, + dn, + amount, + price_shift, +): user = accounts[1] collateral_amount = 10**18 MANY = 10**24 @@ -78,10 +94,10 @@ def test_sell_with_shift(amm, collateral_token, borrowed_token, price_oracle, ac # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - collateral_token._mint_for_testing(amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap max (buy) - borrowed_token._mint_for_testing(user, MANY) + mint_for_testing(borrowed_token, user, MANY) with boa.env.prank(user): amm.exchange(0, 1, MANY, 0) @@ -112,12 +128,22 @@ def test_sell_with_shift(amm, collateral_token, borrowed_token, price_oracle, ac n1=st.integers(min_value=20, max_value=60), # Max is probably unreachable dn=st.integers(min_value=0, max_value=20), amount=st.integers(min_value=1, max_value=10**20), - price_shift=st.floats(min_value=0.1, max_value=10) + price_shift=st.floats(min_value=0.1, max_value=10), ) @settings(max_examples=1000) @example(n1=20, dn=0, amount=4351, price_shift=2.0) # Leaves small dust -def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle, accounts, admin, - n1, dn, amount, price_shift): +def test_no_untradable_funds( + amm, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, + n1, + dn, + amount, + price_shift, +): # Same as buy test at the beginning user = accounts[1] collateral_amount = 10**18 @@ -125,10 +151,10 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - collateral_token._mint_for_testing(amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + mint_for_testing(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -143,12 +169,22 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle price_oracle.set_price(int(price_oracle.price() * price_shift)) # Trade back - collateral_token._mint_for_testing(user, 10**24) # BIG + mint_for_testing(collateral_token, user, 10**24) # BIG with boa.env.prank(user): amm.exchange(1, 0, 10**24, 0) # Check that we cleaned up the last band new_b = borrowed_token.balanceOf(user) - assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf(amm.address), "Insolvent" + if borrowed_token.decimals() == 18: + assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf( + amm.address + ), "Insolvent" + else: + assert ( + 0 + <= borrowed_token.balanceOf(amm.address) + - sum(amm.bands_x(n) for n in range(61)) + <= 1 + ), "Insolvent" assert amm.bands_x(n1) == 0 assert new_b > b @@ -157,12 +193,22 @@ def test_no_untradable_funds(amm, collateral_token, borrowed_token, price_oracle n1=st.integers(min_value=20, max_value=60), # Max is probably unreachable dn=st.integers(min_value=0, max_value=20), amount=st.integers(min_value=1, max_value=10**20), - price_shift=st.floats(min_value=0.1, max_value=10) + price_shift=st.floats(min_value=0.1, max_value=10), ) @settings(max_examples=1000) @example(n1=20, dn=0, amount=4351, price_shift=2.0) # Leaves small dust -def test_no_untradable_funds_in(amm, collateral_token, borrowed_token, price_oracle, accounts, admin, - n1, dn, amount, price_shift): +def test_no_untradable_funds_in( + amm, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, + n1, + dn, + amount, + price_shift, +): # Same as test_no_untradable_funds but with exchange_dy user = accounts[1] collateral_amount = 10**18 @@ -170,10 +216,10 @@ def test_no_untradable_funds_in(amm, collateral_token, borrowed_token, price_ora # Deposit with boa.env.prank(admin): amm.deposit_range(user, collateral_amount, n1, n1 + dn) - collateral_token._mint_for_testing(amm.address, collateral_amount) + mint_for_testing(collateral_token, amm.address, collateral_amount) # Swap stablecoin for collateral - borrowed_token._mint_for_testing(user, amount) + mint_for_testing(borrowed_token, user, amount) with boa.env.prank(user): amm.exchange(0, 1, amount, 0) b = borrowed_token.balanceOf(user) @@ -188,12 +234,22 @@ def test_no_untradable_funds_in(amm, collateral_token, borrowed_token, price_ora price_oracle.set_price(int(price_oracle.price() * price_shift)) # Trade back - collateral_token._mint_for_testing(user, 10**24) # BIG + mint_for_testing(collateral_token, user, 10**24) # BIG with boa.env.prank(user): price_oracle.price_w() amm.exchange_dy(1, 0, 2**256 - 1, 10**24) # Check that we cleaned up the last band new_b = borrowed_token.balanceOf(user) - assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf(amm.address), "Insolvent" + if borrowed_token.decimals() == 18: + assert sum(amm.bands_x(n) for n in range(61)) == borrowed_token.balanceOf( + amm.address + ), "Insolvent" + else: + assert ( + 0 + <= borrowed_token.balanceOf(amm.address) + - sum(amm.bands_x(n) for n in range(61)) + <= 1 + ), "Insolvent" assert amm.bands_x(n1) == 0 assert new_b > b diff --git a/tests/amm/test_price_oracles.py b/tests/amm/test_price_oracles.py index aab50dec..6b62c50c 100644 --- a/tests/amm/test_price_oracles.py +++ b/tests/amm/test_price_oracles.py @@ -1,14 +1,15 @@ import boa import pytest -from ..conftest import PRICE, approx +from ..conftest import PRICE +from tests.utils.deployers import EMA_PRICE_ORACLE_DEPLOYER @pytest.fixture(scope="module") def ema_price_oracle(price_oracle, admin): with boa.env.prank(admin): - signature = price_oracle.price.args_abi_type(0)[0] - signature = b'\x00' * (32 - len(signature)) + signature - return boa.load('contracts/price_oracles/EmaPriceOracle.vy', 10000, price_oracle.address, signature) + selector4 = price_oracle.price.prepare_calldata()[:4] + selector32 = selector4.rjust(32, b"\x00") + return EMA_PRICE_ORACLE_DEPLOYER.deploy(10000, price_oracle.address, selector32) def test_price_oracle(price_oracle, amm): @@ -20,14 +21,14 @@ def test_p_oracle_updown(amm): p_base = amm.get_base_price() A = amm.A() assert amm.p_oracle_up(0) == p_base - assert approx(amm.p_oracle_down(0), p_base * (A - 1) // A, 1e-14) + assert amm.p_oracle_down(0) == pytest.approx(p_base * (A - 1) // A, rel=1e-14) for i in range(-10, 10): mul = ((A - 1) / A) ** i p_up = p_base * mul p_down = p_up * (A - 1) / A - assert approx(amm.p_oracle_up(i), p_up, 1e-14) - assert approx(amm.p_oracle_down(i), p_down, 1e-14) + assert amm.p_oracle_up(i) == pytest.approx(p_up, rel=1e-14) + assert amm.p_oracle_down(i) == pytest.approx(p_down, rel=1e-14) def test_p_current_updown(amm): @@ -41,8 +42,8 @@ def test_p_current_updown(amm): p_base_down = p_base_up * (A - 1) / A p_current_up = p_oracle**3 / p_base_down**2 p_current_down = p_oracle**3 / p_base_up**2 - assert approx(amm.p_current_up(i), p_current_up, 1e-10) - assert approx(amm.p_current_down(i), p_current_down, 1e-10) + assert amm.p_current_up(i) == pytest.approx(p_current_up, rel=1e-10) + assert amm.p_current_down(i) == pytest.approx(p_current_down, rel=1e-10) def test_ema_wrapping(ema_price_oracle, price_oracle): diff --git a/tests/amm/test_share_pump_math.py b/tests/amm/test_share_pump_math.py index d72aa5b8..da9d93fa 100644 --- a/tests/amm/test_share_pump_math.py +++ b/tests/amm/test_share_pump_math.py @@ -3,18 +3,22 @@ SHARE_PRICE = 10**18 -DEAD_SHARES = 1000 # Amounts close to this numbers of shares will experience a small loss (down) +DEAD_SHARES = ( + 1000 # Amounts close to this numbers of shares will experience a small loss (down) +) @given( q1=st.integers(min_value=1, max_value=100000000 * 10**18), # Attacker - q2=st.integers(min_value=1, max_value=10000 * 10**18) # Victim + q2=st.integers(min_value=1, max_value=10000 * 10**18), # Victim ) @settings(max_examples=15000) def test_no_steal(q1, q2): # Protection based on Mixbytes, implemented in an OpenZeppelin audit elsewhere recently # Deposit - s1 = q1 // SHARE_PRICE # Doesn't matter how we've got to this one - could have been pumping + s1 = ( + q1 // SHARE_PRICE + ) # Doesn't matter how we've got to this one - could have been pumping s2 = q2 * (s1 + DEAD_SHARES) // (q1 + 1) if s1 == 0: return diff --git a/tests/amm/test_st_exchange.py b/tests/amm/test_st_exchange.py index 3c77b394..2f044750 100644 --- a/tests/amm/test_st_exchange.py +++ b/tests/amm/test_st_exchange.py @@ -3,18 +3,27 @@ from hypothesis import settings from hypothesis import HealthCheck from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant, initialize +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, + initialize, +) from hypothesis import Phase +from ..utils import mint_for_testing +from tests.utils.deployers import ERC20_MOCK_DEPLOYER class StatefulExchange(RuleBasedStateMachine): - amounts = st.lists(st.integers(min_value=0, max_value=10**6 * 10**18), min_size=5, max_size=5) + amounts = st.lists( + st.integers(min_value=0, max_value=10**6 * 10**18), min_size=5, max_size=5 + ) ns = st.lists(st.integers(min_value=1, max_value=20), min_size=5, max_size=5) dns = st.lists(st.integers(min_value=0, max_value=20), min_size=5, max_size=5) amount = st.integers(min_value=0, max_value=10**9 * 10**18) pump = st.booleans() user_id = st.integers(min_value=0, max_value=4) - admin_fee = st.integers(min_value=0, max_value=10**18 / 2) def __init__(self): super().__init__() @@ -22,17 +31,17 @@ def __init__(self): @initialize(amounts=amounts, ns=ns, dns=dns) def initializer(self, amounts, ns, dns): - self.borrowed_mul = 10**(18 - self.borrowed_digits) - self.collateral_mul = 10**(18 - self.collateral_digits) + self.borrowed_mul = 10 ** (18 - self.borrowed_digits) + self.collateral_mul = 10 ** (18 - self.collateral_digits) amounts = [a // self.collateral_mul for a in amounts] for user, amount, n1, dn in zip(self.accounts, amounts, ns, dns): n2 = n1 + dn try: with boa.env.prank(self.admin): self.amm.deposit_range(user, amount, n1, n2) - self.collateral_token._mint_for_testing(self.amm.address, amount) + mint_for_testing(self.collateral_token, self.amm.address, amount) except Exception as e: - if 'Amount too low' in str(e): + if "Amount too low" in str(e): assert amount // (dn + 1) <= 100 else: raise @@ -53,15 +62,10 @@ def exchange(self, amount, pump, user_id): in_token = self.collateral_token u_amount = in_token.balanceOf(u) if amount > u_amount: - in_token._mint_for_testing(u, amount - u_amount) + mint_for_testing(in_token, u, amount - u_amount) with boa.env.prank(u): self.amm.exchange(i, j, amount, 0) - @rule(fee=admin_fee) - def set_admin_fee(self, fee): - with boa.env.prank(self.admin): - self.amm.set_admin_fee(fee) - @invariant() def amm_solvent(self): X = sum(self.amm.bands_x(n) for n in range(42)) @@ -76,10 +80,12 @@ def dy_back(self): left_in_amm = sum(self.amm.bands_y(n) for n in range(42)) if n < 50: dx, dy = self.amm.get_dxdy(1, 0, to_swap) - assert dx * self.collateral_mul >= self.total_deposited - left_in_amm # With fees, AMM will have more + assert ( + dx * self.collateral_mul >= self.total_deposited - left_in_amm + ) # With fees, AMM will have more def teardown(self): - if not hasattr(self, 'amm'): + if not hasattr(self, "amm"): return u = self.accounts[0] # Trade back and do the check @@ -88,7 +94,7 @@ def teardown(self): if n < 50: _, dy = self.amm.get_dxdy(1, 0, to_swap) if dy > 0: - self.collateral_token._mint_for_testing(u, to_swap) + mint_for_testing(self.collateral_token, u, to_swap) with boa.env.prank(u): self.amm.exchange(1, 0, to_swap, 0) left_in_amm = sum(self.amm.bands_y(n) for n in range(42)) @@ -97,15 +103,17 @@ def teardown(self): @pytest.mark.parametrize("borrowed_digits", [6, 8, 18]) @pytest.mark.parametrize("collateral_digits", [6, 8, 18]) -def test_exchange(admin, accounts, get_amm, get_collateral_token, get_borrowed_token, - borrowed_digits, collateral_digits): - StatefulExchange.TestCase.settings = settings(max_examples=20, stateful_step_count=10, - phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), - suppress_health_check=[HealthCheck.data_too_large]) +def test_exchange(admin, accounts, get_amm, borrowed_digits, collateral_digits): + StatefulExchange.TestCase.settings = settings( + max_examples=20, + stateful_step_count=10, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), + suppress_health_check=[HealthCheck.data_too_large], + ) accounts = accounts[:5] - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): @@ -113,20 +121,22 @@ def test_exchange(admin, accounts, get_amm, get_collateral_token, get_borrowed_t run_state_machine_as_test(StatefulExchange) -def test_raise_at_dy_back(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): +def test_raise_at_dy_back(admin, accounts, get_amm): accounts = accounts[:5] borrowed_digits = 18 collateral_digits = 18 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[0, 0, 0, 10**18, 10**18], ns=[1, 1, 1, 1, 2], dns=[0, 0, 0, 0, 0]) + state.initializer( + amounts=[0, 0, 0, 10**18, 10**18], ns=[1, 1, 1, 1, 2], dns=[0, 0, 0, 0, 0] + ) state.amm_solvent() state.dy_back() state.exchange(amount=3123061067055650168655, pump=True, user_id=0) @@ -138,39 +148,45 @@ def test_raise_at_dy_back(admin, accounts, get_amm, get_collateral_token, get_bo state.teardown() -def test_raise_rounding(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): +def test_raise_rounding(admin, accounts, get_amm): accounts = accounts[:5] borrowed_digits = 16 collateral_digits = 18 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[101, 0, 0, 0, 0], ns=[1, 1, 1, 1, 1], dns=[0, 0, 0, 0, 0]) + state.initializer( + amounts=[101, 0, 0, 0, 0], ns=[1, 1, 1, 1, 1], dns=[0, 0, 0, 0, 0] + ) state.exchange(amount=100, pump=True, user_id=0) state.dy_back() state.teardown() -def test_raise_rounding_2(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): +def test_raise_rounding_2(admin, accounts, get_amm): accounts = accounts[:5] borrowed_digits = 18 collateral_digits = 18 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[779, 5642, 768, 51924, 5], ns=[2, 3, 4, 10, 18], dns=[11, 12, 14, 15, 15]) + state.initializer( + amounts=[779, 5642, 768, 51924, 5], + ns=[2, 3, 4, 10, 18], + dns=[11, 12, 14, 15, 15], + ) state.amm_solvent() state.dy_back() state.exchange(amount=42, pump=True, user_id=1) @@ -182,20 +198,22 @@ def test_raise_rounding_2(admin, accounts, get_amm, get_collateral_token, get_bo state.teardown() -def test_raise_rounding_3(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): +def test_raise_rounding_3(admin, accounts, get_amm): accounts = accounts[:5] borrowed_digits = 17 collateral_digits = 18 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[33477, 63887, 387, 1, 0], ns=[4, 18, 6, 19, 5], dns=[18, 0, 8, 20, 5]) + state.initializer( + amounts=[33477, 63887, 387, 1, 0], ns=[4, 18, 6, 19, 5], dns=[18, 0, 8, 20, 5] + ) state.amm_solvent() state.dy_back() state.exchange(amount=22005, pump=False, user_id=2) diff --git a/tests/amm/test_st_exchange_dy.py b/tests/amm/test_st_exchange_dy.py index c6200163..a5443ca7 100644 --- a/tests/amm/test_st_exchange_dy.py +++ b/tests/amm/test_st_exchange_dy.py @@ -3,8 +3,16 @@ from hypothesis import settings from hypothesis import HealthCheck from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant, initialize +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, + initialize, +) from hypothesis import Phase +from ..utils import mint_for_testing +from tests.utils.deployers import ERC20_MOCK_DEPLOYER class StatefulExchange(RuleBasedStateMachine): @@ -14,7 +22,6 @@ class StatefulExchange(RuleBasedStateMachine): amount = st.floats(min_value=0, max_value=10**9) pump = st.booleans() user_id = st.integers(min_value=0, max_value=4) - admin_fee = st.integers(min_value=0, max_value=10**18 // 2) def __init__(self): super().__init__() @@ -22,17 +29,17 @@ def __init__(self): @initialize(amounts=amounts, ns=ns, dns=dns) def initializer(self, amounts, ns, dns): - self.borrowed_mul = 10**(18 - self.borrowed_digits) - self.collateral_mul = 10**(18 - self.collateral_digits) + self.borrowed_mul = 10 ** (18 - self.borrowed_digits) + self.collateral_mul = 10 ** (18 - self.collateral_digits) amounts = list(map(lambda x: int(x * 10**self.collateral_digits), amounts)) for user, amount, n1, dn in zip(self.accounts, amounts, ns, dns): n2 = n1 + dn try: with boa.env.prank(self.admin): self.amm.deposit_range(user, amount, n1, n2) - self.collateral_token._mint_for_testing(self.amm.address, amount) + mint_for_testing(self.collateral_token, self.amm.address, amount) except Exception as e: - if 'Amount too low' in str(e): + if "Amount too low" in str(e): assert amount // (dn + 1) <= 100 else: raise @@ -54,15 +61,10 @@ def exchange(self, amount, pump, user_id): u_amount = in_token.balanceOf(u) reduced_amount, required_amount = self.amm.get_dydx(i, j, amount) if required_amount > u_amount: - in_token._mint_for_testing(u, required_amount - u_amount) + mint_for_testing(in_token, u, required_amount - u_amount) with boa.env.prank(u): self.amm.exchange_dy(i, j, reduced_amount, required_amount) - @rule(fee=admin_fee) - def set_admin_fee(self, fee): - with boa.env.prank(self.admin): - self.amm.set_admin_fee(fee) - @invariant() def amm_solvent(self): X = sum(self.amm.bands_x(n) for n in range(42)) @@ -73,7 +75,13 @@ def amm_solvent(self): @invariant() def dy_back(self): n = self.amm.active_band() - to_receive = self.total_deposited * self.initial_price * 10 // 10**18 // self.borrowed_mul # Huge amount + to_receive = ( + self.total_deposited + * self.initial_price + * 10 + // 10**18 + // self.borrowed_mul + ) # Huge amount left_in_amm = sum(self.amm.bands_y(n) for n in range(42)) if n < 50: dy, dx = self.amm.get_dydx(1, 0, to_receive) @@ -82,17 +90,25 @@ def dy_back(self): else: with boa.reverts(): self.amm.get_dx(1, 0, to_receive) - assert dx * self.collateral_mul >= self.total_deposited - left_in_amm # With fees, AMM will have more + assert ( + dx * self.collateral_mul >= self.total_deposited - left_in_amm + ) # With fees, AMM will have more def teardown(self): u = self.accounts[0] # Trade back and do the check n = self.amm.active_band() - to_receive = self.total_deposited * self.initial_price * 10 // 10**18 // self.borrowed_mul # Huge amount + to_receive = ( + self.total_deposited + * self.initial_price + * 10 + // 10**18 + // self.borrowed_mul + ) # Huge amount if n < 50: dy, dx = self.amm.get_dydx(1, 0, to_receive) if dy > 0: - self.collateral_token._mint_for_testing(u, dx) + mint_for_testing(self.collateral_token, u, dx) with boa.env.prank(u): self.amm.exchange_dy(1, 0, 2**256 - 1, dx) left_in_amm = sum(self.amm.bands_y(n) for n in range(42)) @@ -101,16 +117,17 @@ def teardown(self): @pytest.mark.parametrize("borrowed_digits", [6, 8, 18]) @pytest.mark.parametrize("collateral_digits", [6, 8, 18]) -def test_exchange(admin, accounts, get_amm, - get_collateral_token, get_borrowed_token, - borrowed_digits, collateral_digits): - StatefulExchange.TestCase.settings = settings(max_examples=20, stateful_step_count=10, - phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), - suppress_health_check=[HealthCheck.data_too_large]) +def test_exchange(admin, accounts, get_amm, borrowed_digits, collateral_digits): + StatefulExchange.TestCase.settings = settings( + max_examples=20, + stateful_step_count=10, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), + suppress_health_check=[HealthCheck.data_too_large], + ) accounts = accounts[:5] - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): @@ -118,22 +135,27 @@ def test_exchange(admin, accounts, get_amm, run_state_machine_as_test(StatefulExchange) -def test_raise_at_dy_back(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): - StatefulExchange.TestCase.settings = settings(max_examples=200, stateful_step_count=10, - phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target)) +def test_raise_at_dy_back(admin, accounts, get_amm): + StatefulExchange.TestCase.settings = settings( + max_examples=200, + stateful_step_count=10, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), + ) accounts = accounts[:5] borrowed_digits = 18 collateral_digits = 18 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[0.0, 0.0, 0.0, 1.0, 1.0], ns=[1, 1, 1, 1, 2], dns=[0, 0, 0, 0, 0]) + state.initializer( + amounts=[0.0, 0.0, 0.0, 1.0, 1.0], ns=[1, 1, 1, 1, 2], dns=[0, 0, 0, 0, 0] + ) state.amm_solvent() state.dy_back() state.exchange(amount=1.0, pump=True, user_id=0) @@ -145,22 +167,35 @@ def test_raise_at_dy_back(admin, accounts, get_amm, get_collateral_token, get_bo state.teardown() -def test_raise_not_enough_left(admin, accounts, get_amm, get_collateral_token, get_borrowed_token): - StatefulExchange.TestCase.settings = settings(max_examples=200, stateful_step_count=10, - phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target)) +def test_raise_not_enough_left(admin, accounts, get_amm): + StatefulExchange.TestCase.settings = settings( + max_examples=200, + stateful_step_count=10, + phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), + ) accounts = accounts[:5] borrowed_digits = 16 collateral_digits = 13 - borrowed_token = get_borrowed_token(borrowed_digits) - collateral_token = get_collateral_token(collateral_digits) + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(borrowed_digits) + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) amm = get_amm(collateral_token, borrowed_token) for k, v in locals().items(): setattr(StatefulExchange, k, v) state = StatefulExchange() - state.initializer(amounts=[419403.0765402276, 1.0, 5e-324, 5.960464477539063e-08, 999999.9999999999], ns=[17, 16, 14, 14, 16], dns=[15, 7, 2, 11, 16]) + state.initializer( + amounts=[ + 419403.0765402276, + 1.0, + 5e-324, + 5.960464477539063e-08, + 999999.9999999999, + ], + ns=[17, 16, 14, 14, 16], + dns=[15, 7, 2, 11, 16], + ) state.amm_solvent() state.dy_back() state.exchange(amount=6.103515625e-05, pump=True, user_id=2) diff --git a/tests/amm/test_xdown_yup_invariants.py b/tests/amm/test_xdown_yup_invariants.py index 3c0efd2b..e1281cbb 100644 --- a/tests/amm/test_xdown_yup_invariants.py +++ b/tests/amm/test_xdown_yup_invariants.py @@ -1,7 +1,9 @@ from hypothesis import given, settings from hypothesis import strategies as st import boa -from ..conftest import approx +import pytest +from ..utils import mint_for_testing + """ Test that get_x_down and get_y_up don't change: * if we do trades at constant p_o (immediate trades) @@ -16,20 +18,33 @@ deposit_amount=st.integers(min_value=10**18, max_value=10**25), f_pump=st.floats(min_value=0, max_value=10), f_trade=st.floats(min_value=0, max_value=10), - is_pump=st.booleans() + is_pump=st.booleans(), ) -def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts, admin, - p_o, n1, dn, deposit_amount, f_pump, f_trade, is_pump): +def test_immediate( + amm, + price_oracle, + collateral_token, + borrowed_token, + accounts, + admin, + p_o, + n1, + dn, + deposit_amount, + f_pump, + f_trade, + is_pump, +): user = accounts[0] with boa.env.prank(admin): price_oracle.set_price(p_o) amm.set_fee(0) - amm.deposit_range(user, deposit_amount, n1, n1+dn) - collateral_token._mint_for_testing(amm.address, deposit_amount) + amm.deposit_range(user, deposit_amount, n1, n1 + dn) + mint_for_testing(collateral_token, amm.address, deposit_amount) pump_amount = int(p_o * deposit_amount / 10**18 * f_pump / 10**12) p_before = amm.get_p() with boa.env.prank(user): - borrowed_token._mint_for_testing(user, pump_amount) + mint_for_testing(borrowed_token, user, pump_amount) boa.env.time_travel(600) # To reset the prev p_o counter amm.exchange(0, 1, pump_amount, 0) while True: @@ -46,13 +61,13 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts if is_pump: trade_amount = int(p_o * deposit_amount / 10**18 * f_trade / 10**12) with boa.env.prank(user): - borrowed_token._mint_for_testing(user, trade_amount) + mint_for_testing(borrowed_token, user, trade_amount) i = 0 j = 1 else: trade_amount = int(deposit_amount * f_trade) with boa.env.prank(user): - collateral_token._mint_for_testing(user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) i = 1 j = 0 @@ -67,26 +82,31 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts assert x1 >= x0 assert y1 >= y0 - fee = max(abs(max(p_after_1, p_after_2, p_before) - p_o), abs(p_o - min(p_after_1, p_after_2, p_before))) / (4 * min(p_after_1, p_after_2, p_before)) + fee = max( + abs(max(p_after_1, p_after_2, p_before) - p_o), + abs(p_o - min(p_after_1, p_after_2, p_before)), + ) / (4 * min(p_after_1, p_after_2, p_before)) - assert approx(x0, x1, fee, 100) - assert approx(y0, y1, fee, 100) + assert x0 == pytest.approx(x1, rel=fee, abs=100) + assert y0 == pytest.approx(y1, rel=fee, abs=100) -def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, accounts, admin): +def test_immediate_above_p0( + amm, price_oracle, collateral_token, borrowed_token, accounts, admin +): deposit_amount = 5805319702344997833315303 user = accounts[0] with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, 6, 6) - collateral_token._mint_for_testing(amm.address, deposit_amount) + mint_for_testing(collateral_token, amm.address, deposit_amount) p_before = amm.get_p() pump_amount = 3000 * deposit_amount * 147 // 10**18 // 10**12 with boa.env.prank(user): - borrowed_token._mint_for_testing(user, pump_amount) + mint_for_testing(borrowed_token, user, pump_amount) amm.exchange(0, 1, pump_amount, 0) p_after_1 = amm.get_p() @@ -94,7 +114,7 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, y0 = amm.get_y_up(user) trade_amount = deposit_amount * 52469 // 10**18 - collateral_token._mint_for_testing(user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) with boa.env.prank(user): amm.exchange(1, 0, trade_amount, 0) @@ -108,27 +128,31 @@ def test_immediate_above_p0(amm, price_oracle, collateral_token, borrowed_token, assert x1 >= x0 assert y1 >= y0 - fee = max(abs(p_after_1 - p_before), abs(p_after_2 - p_before)) / (4 * min(p_after_1, p_after_2, p_before)) + fee = max(abs(p_after_1 - p_before), abs(p_after_2 - p_before)) / ( + 4 * min(p_after_1, p_after_2, p_before) + ) - assert approx(y0, deposit_amount, fee) - assert approx(x0, x1, fee) - assert approx(y0, y1, fee) + assert y0 == pytest.approx(deposit_amount, rel=fee, abs=1) + assert x0 == pytest.approx(x1, rel=fee) + assert y0 == pytest.approx(y1, rel=fee) -def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, accounts, admin): +def test_immediate_in_band( + amm, price_oracle, collateral_token, borrowed_token, accounts, admin +): deposit_amount = 835969548449222546344625 user = accounts[0] with boa.env.prank(admin): amm.set_fee(0) amm.deposit_range(user, deposit_amount, 4, 4) - collateral_token._mint_for_testing(amm.address, deposit_amount) + mint_for_testing(collateral_token, amm.address, deposit_amount) p_before = amm.get_p() pump_amount = 137 with boa.env.prank(user): - borrowed_token._mint_for_testing(user, pump_amount) + mint_for_testing(borrowed_token, user, pump_amount) amm.exchange(0, 1, pump_amount, 0) p_after_1 = amm.get_p() @@ -137,7 +161,7 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, trade_amount = 2690425910633510 # 181406004646580 with boa.env.prank(user): - borrowed_token._mint_for_testing(user, trade_amount) + mint_for_testing(borrowed_token, user, trade_amount) amm.exchange(0, 1, trade_amount, 0) p_after_2 = amm.get_p() @@ -149,11 +173,13 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, assert x1 >= x0 assert y1 >= y0 - fee = max(abs(p_after_1 - p_before), abs(p_after_2 - p_before)) / (4 * min(p_after_1, p_after_2, p_before)) + fee = max(abs(p_after_1 - p_before), abs(p_after_2 - p_before)) / ( + 4 * min(p_after_1, p_after_2, p_before) + ) - assert approx(y0, deposit_amount, fee) - assert approx(x0, x1, fee) - assert approx(y0, y1, fee) + assert y0 == pytest.approx(deposit_amount, rel=fee) + assert x0 == pytest.approx(x1, rel=fee) + assert y0 == pytest.approx(y1, rel=fee) @given( @@ -164,15 +190,26 @@ def test_immediate_in_band(amm, price_oracle, collateral_token, borrowed_token, deposit_amount=st.integers(min_value=10**18, max_value=10**25), ) @settings(max_examples=100) -def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts, admin, - p_o_1, p_o_2, n1, dn, deposit_amount): +def test_adiabatic( + amm, + price_oracle, + collateral_token, + borrowed_token, + accounts, + admin, + p_o_1, + p_o_2, + n1, + dn, + deposit_amount, +): N_STEPS = 101 user = accounts[0] with boa.env.prank(admin): amm.set_fee(0) - amm.deposit_range(user, deposit_amount, dn, n1+dn) - collateral_token._mint_for_testing(amm.address, deposit_amount) + amm.deposit_range(user, deposit_amount, dn, n1 + dn) + mint_for_testing(collateral_token, amm.address, deposit_amount) for i in range(2): boa.env.time_travel(600) price_oracle.set_price(p_o_1) @@ -180,9 +217,17 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts p_o = p_o_1 p_o_mul = (p_o_2 / p_o_1) ** (1 / (N_STEPS - 1)) - precision = max(1.5 * abs(p_o_mul - 1) * (dn + 1) * (max(p_o_2, p_o_1) / min(p_o_2, p_o_1)), 1e-6) # Emprical formula - precision += 1 - min(p_o_mul, 1 / p_o_mul)**3 # Dynamic fee component - fee_component = 2 * (max(p_o_1, p_o_2, 3000 * 10**18) - min(p_o_1, p_o_2, 3000 * 10**18)) / min(p_o_1, p_o_2, 3000 * 10**18) / N_STEPS + precision = max( + 1.5 * abs(p_o_mul - 1) * (dn + 1) * (max(p_o_2, p_o_1) / min(p_o_2, p_o_1)), + 1e-6, + ) # Emprical formula + precision += 1 - min(p_o_mul, 1 / p_o_mul) ** 3 # Dynamic fee component + fee_component = ( + 2 + * (max(p_o_1, p_o_2, 3000 * 10**18) - min(p_o_1, p_o_2, 3000 * 10**18)) + / min(p_o_1, p_o_2, 3000 * 10**18) + / N_STEPS + ) x0 = 0 y0 = 0 @@ -198,11 +243,11 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts if is_pump: i = 0 j = 1 - borrowed_token._mint_for_testing(user, amount) + mint_for_testing(borrowed_token, user, amount) else: i = 1 j = 0 - collateral_token._mint_for_testing(user, amount) + mint_for_testing(collateral_token, user, amount) with boa.env.prank(user): amm.exchange(i, j, amount, 0) @@ -217,15 +262,27 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts assert x >= x0 * (1 - precision) assert y >= y0 * (1 - precision) - assert approx(x, x0, precision + fee_component * (k + 1)) - assert approx(y, y0, precision + fee_component * (k + 1)) + assert x == pytest.approx(x0, rel=precision + fee_component * (k + 1)) + assert y == pytest.approx(y0, rel=precision + fee_component * (k + 1)) if k != N_STEPS - 1: p_o = int(p_o * p_o_mul) -def test_adiabatic_fail_1(amm, price_oracle, collateral_token, borrowed_token, accounts, admin): +def test_adiabatic_fail_1( + amm, price_oracle, collateral_token, borrowed_token, accounts, admin +): with boa.env.anchor(): test_adiabatic.hypothesis.inner_test( - amm, price_oracle, collateral_token, borrowed_token, accounts, admin, - p_o_1=2296376199582847058288, p_o_2=2880636282130384399567, n1=19, dn=0, deposit_amount=1000000000000000000) + amm, + price_oracle, + collateral_token, + borrowed_token, + accounts, + admin, + p_o_1=2296376199582847058288, + p_o_2=2880636282130384399567, + n1=19, + dn=0, + deposit_amount=1000000000000000000, + ) diff --git a/tests/amm/test_xdown_yup_invariants_dy.py b/tests/amm/test_xdown_yup_invariants_dy.py index 48815600..d9e91dce 100644 --- a/tests/amm/test_xdown_yup_invariants_dy.py +++ b/tests/amm/test_xdown_yup_invariants_dy.py @@ -2,7 +2,8 @@ from hypothesis import strategies as st import boa import pytest -from ..conftest import approx +from ..utils import mint_for_testing + """ Test that get_x_down and get_y_up don't change: * if we do trades at constant p_o (immediate trades) @@ -10,11 +11,6 @@ """ -@pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) - - @pytest.fixture(scope="module") def amm(get_amm, borrowed_token, collateral_token): return get_amm(collateral_token, borrowed_token) @@ -27,10 +23,23 @@ def amm(get_amm, borrowed_token, collateral_token): deposit_amount=st.floats(min_value=1e-9, max_value=1e7), f_pump=st.floats(min_value=0, max_value=10), f_trade=st.floats(min_value=0, max_value=10), - is_pump=st.booleans() + is_pump=st.booleans(), ) -def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts, admin, - p_o, n1, dn, deposit_amount, f_pump, f_trade, is_pump): +def test_immediate( + amm, + price_oracle, + collateral_token, + borrowed_token, + accounts, + admin, + p_o, + n1, + dn, + deposit_amount, + f_pump, + f_trade, + is_pump, +): collateral_decimals = collateral_token.decimals() borrowed_decimals = borrowed_token.decimals() deposit_amount = int(deposit_amount * 10**collateral_decimals) @@ -40,8 +49,8 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts with boa.env.prank(admin): price_oracle.set_price(p_o) amm.set_fee(0) - amm.deposit_range(user, deposit_amount, n1, n1+dn) - collateral_token._mint_for_testing(amm.address, deposit_amount) + amm.deposit_range(user, deposit_amount, n1, n1 + dn) + mint_for_testing(collateral_token, amm.address, deposit_amount) while True: p_internal = amm.price_oracle() boa.env.time_travel(600) # To reset the prev p_o counter @@ -53,7 +62,7 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts pump_recv_amount = int(deposit_amount * f_pump) pump_recv_amount, pump_amount = amm.get_dydx(0, 1, pump_recv_amount) with boa.env.prank(user): - borrowed_token._mint_for_testing(user, pump_amount) + mint_for_testing(borrowed_token, user, pump_amount) amm.exchange_dy(0, 1, pump_recv_amount, pump_amount) prices.append(amm.get_p()) @@ -63,14 +72,20 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts trade_recv_amount = int(deposit_amount * f_trade) trade_recv_amount, trade_amount = amm.get_dydx(0, 1, trade_recv_amount) with boa.env.prank(user): - borrowed_token._mint_for_testing(user, trade_amount) + mint_for_testing(borrowed_token, user, trade_amount) i = 0 j = 1 else: - trade_recv_amount = int(p_o * deposit_amount / 10**collateral_decimals * f_trade / 10**(collateral_decimals - borrowed_decimals)) + trade_recv_amount = int( + p_o + * deposit_amount + / 10**collateral_decimals + * f_trade + / 10 ** (collateral_decimals - borrowed_decimals) + ) trade_recv_amount, trade_amount = amm.get_dydx(1, 0, trade_recv_amount) with boa.env.prank(user): - collateral_token._mint_for_testing(user, trade_amount) + mint_for_testing(collateral_token, user, trade_amount) i = 1 j = 0 @@ -86,8 +101,8 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts fee = max(abs(max(prices) - p_o), abs(p_o - min(prices))) / (4 * min(p_o, *prices)) - assert approx(x0, x1, fee, 100) - assert approx(y0, y1, fee, 100) + assert x0 == pytest.approx(x1, rel=fee, abs=100) + assert y0 == pytest.approx(y1, rel=fee, abs=100) @given( @@ -98,17 +113,28 @@ def test_immediate(amm, price_oracle, collateral_token, borrowed_token, accounts deposit_amount=st.floats(min_value=1, max_value=1e7), ) @settings(max_examples=100) -def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts, admin, - p_o_1, p_o_2, n1, dn, deposit_amount): +def test_adiabatic( + amm, + price_oracle, + collateral_token, + borrowed_token, + accounts, + admin, + p_o_1, + p_o_2, + n1, + dn, + deposit_amount, +): collateral_decimals = collateral_token.decimals() - deposit_amount = int(deposit_amount * 10 ** collateral_decimals) + deposit_amount = int(deposit_amount * 10**collateral_decimals) N_STEPS = 101 user = accounts[0] with boa.env.prank(admin): amm.set_fee(0) - amm.deposit_range(user, deposit_amount, dn, n1+dn) - collateral_token._mint_for_testing(amm.address, deposit_amount) + amm.deposit_range(user, deposit_amount, dn, n1 + dn) + mint_for_testing(collateral_token, amm.address, deposit_amount) for i in range(2): boa.env.time_travel(600) price_oracle.set_price(p_o_1) @@ -116,9 +142,17 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts p_o = p_o_1 p_o_mul = (p_o_2 / p_o_1) ** (1 / (N_STEPS - 1)) - precision = max(1.5 * abs(p_o_mul - 1) * (dn + 1) * (max(p_o_2, p_o_1) / min(p_o_2, p_o_1)), 1e-6) # Emprical formula - precision += 1 - min(p_o_mul, 1 / p_o_mul)**3 # Dynamic fee component - fee_component = 2 * (max(p_o_1, p_o_2, 3000 * 10**18) - min(p_o_1, p_o_2, 3000 * 10**18)) / min(p_o_1, p_o_2, 3000 * 10**18) / N_STEPS + precision = max( + 1.5 * abs(p_o_mul - 1) * (dn + 1) * (max(p_o_2, p_o_1) / min(p_o_2, p_o_1)), + 1e-6, + ) # Emprical formula + precision += 1 - min(p_o_mul, 1 / p_o_mul) ** 3 # Dynamic fee component + fee_component = ( + 2 + * (max(p_o_1, p_o_2, 3000 * 10**18) - min(p_o_1, p_o_2, 3000 * 10**18)) + / min(p_o_1, p_o_2, 3000 * 10**18) + / N_STEPS + ) x0 = 0 y0 = 0 @@ -136,13 +170,13 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts j = 1 recv_amount = amm.get_dy(i, j, amount) _amount = amm.get_dx(i, j, recv_amount) - borrowed_token._mint_for_testing(user, _amount) + mint_for_testing(borrowed_token, user, _amount) else: i = 1 j = 0 recv_amount = amm.get_dy(i, j, amount) _amount = amm.get_dx(i, j, recv_amount) - collateral_token._mint_for_testing(user, _amount) + mint_for_testing(collateral_token, user, _amount) with boa.env.prank(user): amm.exchange_dy(i, j, recv_amount, _amount) @@ -157,8 +191,8 @@ def test_adiabatic(amm, price_oracle, collateral_token, borrowed_token, accounts assert x >= x0 * (1 - precision) assert y >= y0 * (1 - precision) - assert approx(x, x0, precision + fee_component * (k + 1)) - assert approx(y, y0, precision + fee_component * (k + 1)) + assert x == pytest.approx(x0, rel=precision + fee_component * (k + 1)) + assert y == pytest.approx(y0, rel=precision + fee_component * (k + 1)) if k != N_STEPS - 1: p_o = int(p_o * p_o_mul) diff --git a/tests/boosted_lm_callback/conftest.py b/tests/boosted_lm_callback/conftest.py deleted file mode 100644 index 715a69ce..00000000 --- a/tests/boosted_lm_callback/conftest.py +++ /dev/null @@ -1,157 +0,0 @@ -import boa -import pytest - - -@pytest.fixture(scope="module") -def crv(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20CRV.vy', "Curve DAO Token", "CRV", 18) - - -@pytest.fixture(scope="module") -def voting_escrow(admin, crv): - with boa.env.prank(admin): - return boa.load('contracts/testing/VotingEscrow.vy', crv, "Voting-escrowed CRV", "veCRV", "veCRV_0.99") - - -@pytest.fixture(scope="module") -def voting_escrow_delegation_mock(admin, voting_escrow): - with boa.env.prank(admin): - return boa.load('contracts/testing/VEDelegationMock.vy', voting_escrow) - - -@pytest.fixture(scope="module") -def gauge_controller(admin, crv, voting_escrow): - with boa.env.prank(admin): - return boa.load('contracts/testing/GaugeController.vy', crv, voting_escrow) - - -@pytest.fixture(scope="module") -def minter(admin, crv, gauge_controller): - with boa.env.prank(admin): - return boa.load('contracts/testing/Minter.vy', crv, gauge_controller) - - -# Trader -@pytest.fixture(scope="module") -def chad(collateral_token, admin): - _chad = boa.env.generate_address() - collateral_token._mint_for_testing(_chad, 10**25, sender=admin) - - return _chad - - -@pytest.fixture(scope="module") -def stablecoin(admin, chad): - with boa.env.prank(admin): - _stablecoin = boa.load('contracts/Stablecoin.vy', 'Curve USD', 'crvUSD') - _stablecoin.mint(chad, 10**25) - - return _stablecoin - - -@pytest.fixture(scope="module") -def weth(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/WETH.vy') - - -@pytest.fixture(scope="module") -def controller_prefactory(stablecoin, weth, admin, accounts): - with boa.env.prank(admin): - return boa.load('contracts/ControllerFactory.vy', stablecoin.address, admin, accounts[0], weth.address) - - -@pytest.fixture(scope="module") -def controller_interface(): - return boa.load_partial('contracts/Controller.vy') - - -@pytest.fixture(scope="module") -def controller_impl(controller_prefactory, controller_interface, admin): - with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="module") -def amm_interface(): - return boa.load_partial('contracts/AMM.vy') - - -@pytest.fixture(scope="module") -def amm_impl(stablecoin, amm_interface, admin): - with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="module") -def controller_factory(controller_prefactory, amm_impl, controller_impl, stablecoin, admin): - with boa.env.prank(admin): - controller_prefactory.set_implementations(controller_impl.address, amm_impl.address) - stablecoin.set_minter(controller_prefactory.address) - return controller_prefactory - - -@pytest.fixture(scope="module") -def monetary_policy(admin): - with boa.env.prank(admin): - policy = boa.load('contracts/testing/ConstantMonetaryPolicy.vy', admin) - policy.set_rate(0) - return policy - - -@pytest.fixture(scope="module") -def get_market(controller_factory, monetary_policy, price_oracle, stablecoin, accounts, admin, chad): - def f(collateral_token): - with boa.env.prank(admin): - if controller_factory.n_collaterals() == 0: - controller_factory.add_market( - collateral_token.address, 100, 10**16, 0, - price_oracle.address, - monetary_policy.address, 5 * 10**16, 2 * 10**16, - 10**8 * 10**18) - amm = controller_factory.get_amm(collateral_token.address) - controller = controller_factory.get_controller(collateral_token.address) - for acc in accounts: - with boa.env.prank(acc): - collateral_token.approve(amm, 2**256-1) - stablecoin.approve(amm, 2**256-1) - collateral_token.approve(controller, 2**256-1) - stablecoin.approve(controller, 2**256-1) - with boa.env.prank(chad): - collateral_token.approve(amm, 2 ** 256 - 1) - stablecoin.approve(amm, 2 ** 256 - 1) - return controller_factory - return f - - -@pytest.fixture(scope="module") -def market(get_market, collateral_token): - return get_market(collateral_token) - - -@pytest.fixture(scope="module") -def market_amm(market, collateral_token, stablecoin, amm_interface, accounts): - return amm_interface.at(market.get_amm(collateral_token.address)) - - -@pytest.fixture(scope="module") -def market_controller(market, stablecoin, collateral_token, controller_interface, controller_factory, accounts): - return controller_interface.at(market.get_controller(collateral_token.address)) - - -@pytest.fixture(scope="module") -def boosted_lm_callback(admin, market_amm, crv, voting_escrow, voting_escrow_delegation_mock, - gauge_controller, minter, market_controller): - with boa.env.prank(admin): - cb = boa.load('contracts/BoostedLMCallback.vy', market_amm, crv, - voting_escrow, voting_escrow_delegation_mock, gauge_controller, minter) - market_controller.set_callback(cb) - - return cb - - -@pytest.fixture(scope="module") -def block_counter(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/BlockCounter.vy') diff --git a/tests/boosted_lm_callback/test_as_gauge.py b/tests/boosted_lm_callback/test_as_gauge.py deleted file mode 100644 index 985e4d7a..00000000 --- a/tests/boosted_lm_callback/test_as_gauge.py +++ /dev/null @@ -1,324 +0,0 @@ -import boa -from random import random, randrange -from ..conftest import approx - -MAX_UINT256 = 2 ** 256 - 1 -YEAR = 365 * 86400 -WEEK = 7 * 86400 - - -def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, boosted_lm_callback, gauge_controller, market_controller): - with boa.env.anchor(): - alice = accounts[0] - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - boa.env.time_travel(seconds=WEEK) - alice_staked = 0 - integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp - # boa.env.time_travel(blocks=1) - checkpoint_rate = crv.rate() - checkpoint_supply = 0 - checkpoint_balance = 0 - - collateral_token._mint_for_testing(alice, 1000 * 10**18, sender=admin) - - def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp - rate1 = crv.rate() - t_epoch = crv.start_epoch_time_write(sender=admin) - if checkpoint >= t_epoch: - rate_x_time = (t1 - checkpoint) * rate1 - else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 - if checkpoint_supply > 0: - integral += rate_x_time * checkpoint_balance // checkpoint_supply - checkpoint_rate = rate1 - checkpoint = t1 - checkpoint_supply = boosted_lm_callback.total_collateral() - checkpoint_balance = boosted_lm_callback.user_collateral(alice) - - for i in range(40): - dt = 3 * (i + 1) * 86400 - boa.env.time_travel(seconds=dt) - - is_withdraw = (i > 0) * (random() < 0.5) - with boa.env.prank(alice): - collateral_in_amm, _, debt, __ = market_controller.user_state(alice) - collateral_alice = boosted_lm_callback.user_collateral(alice) - assert collateral_in_amm == collateral_alice - print("Alice", "withdraws" if is_withdraw else "deposits") - - if is_withdraw: - amount = randrange(1, collateral_in_amm + 1) - if amount == collateral_in_amm: - market_controller.repay(debt) - else: - repay_amount = int(debt * random() * 0.99) - market_controller.repay(repay_amount) - min_collateral_required = market_controller.min_collateral(debt - repay_amount, 10) - remove_amount = min(collateral_in_amm - min_collateral_required, amount) - remove_amount = max(remove_amount, 0) - if remove_amount > 0: - market_controller.remove_collateral(remove_amount) - update_integral() - alice_staked -= remove_amount - else: - amount = collateral_token.balanceOf(alice) // 5 - collateral_token.approve(market_controller.address, amount) - if market_controller.loan_exists(alice): - market_controller.borrow_more(amount, int(amount * random() * 2000)) - else: - market_controller.create_loan(amount, int(amount * random() * 2000), 10) - update_integral() - alice_staked += amount - - assert boosted_lm_callback.user_collateral(alice) == alice_staked - assert boosted_lm_callback.total_collateral() == alice_staked - - dt = (i + 1) * 10 * 86400 - boa.env.time_travel(seconds=dt) - - boosted_lm_callback.user_checkpoint(alice, sender=alice) - update_integral() - print(i, dt / 86400, integral, boosted_lm_callback.integrate_fraction(alice)) - if integral > 0: - assert approx(boosted_lm_callback.integrate_fraction(alice), integral, 1e-14) - - -def test_gauge_integral(accounts, admin, collateral_token, crv, boosted_lm_callback, gauge_controller, market_controller): - with boa.env.anchor(): - alice, bob = accounts[:2] - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - alice_staked = 0 - bob_staked = 0 - integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp - boa.env.time_travel(blocks=1) - checkpoint_rate = crv.rate() - checkpoint_supply = 0 - checkpoint_balance = 0 - - # Let Alice and Bob have about the same collateral token amount - with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) - - def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp - rate1 = crv.rate() - t_epoch = crv.start_epoch_time() - if checkpoint >= t_epoch: - rate_x_time = (t1 - checkpoint) * rate1 - else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 - if checkpoint_supply > 0: - integral += rate_x_time * checkpoint_balance // checkpoint_supply - checkpoint_rate = rate1 - checkpoint = t1 - checkpoint_supply = boosted_lm_callback.total_collateral() - checkpoint_balance = boosted_lm_callback.user_collateral(alice) - - # Now let's have a loop where Bob always deposit or withdraws, - # and Alice does so more rarely - for i in range(40): - is_alice = random() < 0.2 - dt = randrange(1, YEAR // 5) - boa.env.time_travel(seconds=dt) - - # For Bob - with boa.env.prank(bob): - is_withdraw_bob = (i > 0) * (random() < 0.5) - print("Bob", "withdraws" if is_withdraw_bob else "deposits") - if is_withdraw_bob: - collateral_in_amm_bob, _, debt_bob, __ = market_controller.user_state(bob) - collateral_bob = boosted_lm_callback.user_collateral(bob) - assert collateral_in_amm_bob == collateral_bob - amount_bob = randrange(1, collateral_in_amm_bob + 1) - remove_amount_bob = amount_bob - if amount_bob == collateral_in_amm_bob: - market_controller.repay(debt_bob) - else: - repay_amount_bob = int(debt_bob * random() * 0.99) - market_controller.repay(repay_amount_bob) - min_collateral_required_bob = market_controller.min_collateral(debt_bob - repay_amount_bob, 10) - remove_amount_bob = min(collateral_in_amm_bob - min_collateral_required_bob, amount_bob) - remove_amount_bob = max(remove_amount_bob, 0) - if remove_amount_bob > 0: - market_controller.remove_collateral(remove_amount_bob) - update_integral() - bob_staked -= remove_amount_bob - else: - amount_bob = randrange(1, collateral_token.balanceOf(bob) // 10 + 1) - collateral_token.approve(market_controller.address, amount_bob) - if market_controller.loan_exists(bob): - market_controller.borrow_more(amount_bob, int(amount_bob * random() * 2000)) - else: - market_controller.create_loan(amount_bob, int(amount_bob * random() * 2000), 10) - update_integral() - bob_staked += amount_bob - - if is_alice: - # For Alice - with boa.env.prank(alice): - collateral_in_amm_alice, _, debt_alice, __ = market_controller.user_state(alice) - collateral_alice = boosted_lm_callback.user_collateral(alice) - assert collateral_in_amm_alice == collateral_alice - is_withdraw_alice = (collateral_in_amm_alice > 0) * (random() < 0.5) - print("Alice", "withdraws" if is_withdraw_alice else "deposits") - - if is_withdraw_alice: - amount_alice = randrange(1, collateral_in_amm_alice + 1) - remove_amount_alice = amount_alice - if amount_alice == collateral_in_amm_alice: - market_controller.repay(debt_alice) - else: - repay_amount_alice = int(debt_alice * random() * 0.99) - market_controller.repay(repay_amount_alice) - min_collateral_required_alice = market_controller.min_collateral(debt_alice - repay_amount_alice, 10) - remove_amount_alice = min(collateral_in_amm_alice - min_collateral_required_alice, amount_alice) - remove_amount_alice = max(remove_amount_alice, 0) - if remove_amount_alice > 0: - market_controller.remove_collateral(remove_amount_alice) - update_integral() - alice_staked -= remove_amount_alice - else: - amount_alice = randrange(1, collateral_token.balanceOf(alice) // 10 + 1) - collateral_token.approve(market_controller.address, amount_alice) - if market_controller.loan_exists(alice): - market_controller.borrow_more(amount_alice, int(amount_alice * random() * 2000)) - else: - market_controller.create_loan(amount_alice, int(amount_alice * random() * 2000), 10) - update_integral() - alice_staked += amount_alice - - # Checking that updating the checkpoint in the same second does nothing - # Also everyone can update: that should make no difference, too - if random() < 0.5: - boosted_lm_callback.user_checkpoint(alice, sender=alice) - if random() < 0.5: - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - assert boosted_lm_callback.user_collateral(alice) == alice_staked - assert boosted_lm_callback.user_collateral(bob) == bob_staked - assert boosted_lm_callback.total_collateral() == alice_staked + bob_staked - - dt = randrange(1, YEAR // 20) - boa.env.time_travel(seconds=dt) - - boosted_lm_callback.user_checkpoint(alice, sender=alice) - update_integral() - print(i, dt / 86400, integral, boosted_lm_callback.integrate_fraction(alice)) - assert approx(boosted_lm_callback.integrate_fraction(alice), integral, 1e-14) - - -def test_mining_with_votelock( - accounts, - admin, - collateral_token, - crv, - market_controller, - boosted_lm_callback, - gauge_controller, - voting_escrow, -): - alice, bob = accounts[:2] - boa.env.time_travel(seconds=2 * WEEK + 5) - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - # Let Alice and Bob have about the same amount of CRV - with boa.env.prank(admin): - crv.transfer(alice, 10 ** 20) - crv.transfer(bob, 10 ** 20) - crv.approve(voting_escrow, MAX_UINT256, sender=alice) - crv.approve(voting_escrow, MAX_UINT256, sender=bob) - - # Let Alice and Bob have about the same collateral token amount - with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) - collateral_token._mint_for_testing(bob, 1000 * 10 ** 18) - - collateral_token.approve(market_controller.address, MAX_UINT256, sender=alice) - collateral_token.approve(market_controller.address, MAX_UINT256, sender=bob) - - # Alice deposits to escrow. She now has a BOOST - t = boa.env.evm.patch.timestamp - voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, sender=alice) - - # Alice and Bob create loan - market_controller.create_loan(10**21, 10**21 * 1000, 10, sender=alice) - market_controller.create_loan(10**21, 10**21 * 1000, 10, sender=bob) - - # Time travel and checkpoint - boa.env.time_travel(4 * WEEK) - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - # 4 weeks down the road, balanceOf must be 0 - assert voting_escrow.balanceOf(alice) == 0 - assert voting_escrow.balanceOf(bob) == 0 - - # Alice earned 2.5 times more CRV because she vote-locked her CRV - rewards_alice = boosted_lm_callback.integrate_fraction(alice) - rewards_bob = boosted_lm_callback.integrate_fraction(bob) - assert approx(rewards_alice / rewards_bob, 2.5, 1e-14) - - # Time travel / checkpoint: no one has CRV vote-locked - boa.env.time_travel(4 * WEEK) - voting_escrow.withdraw(sender=alice) - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - old_rewards_alice = rewards_alice - old_rewards_bob = rewards_bob - - # Alice earned the same as Bob now - rewards_alice = boosted_lm_callback.integrate_fraction(alice) - rewards_bob = boosted_lm_callback.integrate_fraction(bob) - d_alice = rewards_alice - old_rewards_alice - d_bob = rewards_bob - old_rewards_bob - assert d_alice == d_bob - - # Both Alice and Bob votelock - t = boa.env.evm.patch.timestamp - voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, sender=alice) - voting_escrow.create_lock(10 ** 20, t + 2 * WEEK, sender=bob) - - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - # Time travel / checkpoint: no one has CRV vote-locked - boa.env.time_travel(4 * WEEK) - voting_escrow.withdraw(sender=alice) - voting_escrow.withdraw(sender=bob) - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - old_rewards_alice = rewards_alice - old_rewards_bob = rewards_bob - - # Alice earned the same as Bob now - rewards_alice = boosted_lm_callback.integrate_fraction(alice) - rewards_bob = boosted_lm_callback.integrate_fraction(bob) - d_alice = rewards_alice - old_rewards_alice - d_bob = rewards_bob - old_rewards_bob - assert d_alice == d_bob diff --git a/tests/boosted_lm_callback/test_lm_callback.py b/tests/boosted_lm_callback/test_lm_callback.py deleted file mode 100644 index eddb9eb3..00000000 --- a/tests/boosted_lm_callback/test_lm_callback.py +++ /dev/null @@ -1,348 +0,0 @@ -import boa -from random import random, randrange, choice -from ..conftest import approx - -MAX_UINT256 = 2 ** 256 - 1 -YEAR = 365 * 86400 -WEEK = 7 * 86400 - - -def test_simple_exchange( - accounts, - admin, - chad, - collateral_token, - crv, - market_controller, - market_amm, - boosted_lm_callback, - gauge_controller, - voting_escrow, -): - alice, bob = accounts[:2] - boa.env.time_travel(seconds=2 * WEEK + 5) - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - # Let Alice and Bob have about the same collateral token amount - with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) - collateral_token._mint_for_testing(bob, 1000 * 10 ** 18) - - # Alice and Bob create loan - market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) - market_controller.create_loan(10**21, 10**21 * 1000, 10, sender=bob) - - # Time travel and checkpoint - boa.env.time_travel(4 * WEEK) - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - rewards_alice = boosted_lm_callback.integrate_fraction(alice) - rewards_bob = boosted_lm_callback.integrate_fraction(bob) - assert rewards_alice == rewards_bob - - # Now Chad makes a trade crvUSD --> collateral and gets a half of Alice's deposit - market_amm.exchange_dy(0, 1, 10**21 // 2, 2**255, sender=chad) - - # Time travel and checkpoint - boa.env.time_travel(4 * WEEK) - boosted_lm_callback.user_checkpoint(alice, sender=alice) - boosted_lm_callback.user_checkpoint(bob, sender=bob) - old_rewards_alice = rewards_alice - old_rewards_bob = rewards_bob - - # Bob earned 2 times more CRV - rewards_alice = boosted_lm_callback.integrate_fraction(alice) - rewards_bob = boosted_lm_callback.integrate_fraction(bob) - d_alice = rewards_alice - old_rewards_alice - d_bob = rewards_bob - old_rewards_bob - assert approx(d_bob / d_alice, 2, 1e-15) - - -def test_gauge_integral_with_exchanges( - accounts, - admin, - chad, - collateral_token, - crv, - boosted_lm_callback, - gauge_controller, - market_controller, - market_amm, - price_oracle, -): - with boa.env.anchor(): - alice, bob = accounts[:2] - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp - checkpoint_rate = crv.rate() - checkpoint_supply = 0 - checkpoint_balance = 0 - - boa.env.time_travel(seconds=WEEK) - - # Let Alice and Bob have about the same collateral token amount - with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) - - def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp - t_epoch = crv.start_epoch_time_write(sender=admin) - rate1 = crv.rate() - if checkpoint >= t_epoch: - rate_x_time = (t1 - checkpoint) * rate1 - else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 - if checkpoint_supply > 0: - integral += rate_x_time * checkpoint_balance // checkpoint_supply - checkpoint_rate = rate1 - checkpoint = t1 - checkpoint_supply = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() - checkpoint_balance = market_amm.get_sum_xy(alice)[1] - - # Now let's have a loop where Bob always deposit or withdraws, - # and Alice does so more rarely - for i in range(40): - is_alice = random() < 0.2 - dt = randrange(1, YEAR // 5) - boa.env.time_travel(seconds=dt) - print("Time travel", dt) - - # For Bob - with boa.env.prank(bob): - collateral_in_amm_bob, stablecoin_in_amm_bob, debt_bob, __ = market_controller.user_state(bob) - is_withdraw_bob = (collateral_in_amm_bob > 0) * (random() < 0.5) - is_underwater_bob = stablecoin_in_amm_bob > 0 - - if is_withdraw_bob: - amount_bob = randrange(1, collateral_in_amm_bob + 1) - if amount_bob == collateral_in_amm_bob: - print("Bob repays (full):", debt_bob) - print("Bob withdraws (full):", amount_bob) - market_controller.repay(debt_bob) - assert approx(market_amm.get_sum_xy(bob)[1], boosted_lm_callback.user_collateral(bob), 1e-13) - assert approx(market_amm.get_sum_xy(bob)[1] * 4 // 10, boosted_lm_callback.working_collateral(bob), 1e-13) - elif market_controller.health(bob) > 0: - repay_amount_bob = int(debt_bob // 10 + (debt_bob * 9 // 10) * random() * 0.99) - print("Bob repays:", repay_amount_bob) - market_controller.repay(repay_amount_bob) - if not is_underwater_bob: - min_collateral_required_bob = market_controller.min_collateral(debt_bob - repay_amount_bob, 10) - remove_amount_bob = min(collateral_in_amm_bob - min_collateral_required_bob, amount_bob) - remove_amount_bob = max(remove_amount_bob, 0) - if remove_amount_bob > 0: - print("Bob withdraws:", remove_amount_bob) - market_controller.remove_collateral(remove_amount_bob) - assert approx(market_amm.get_sum_xy(bob)[1], boosted_lm_callback.user_collateral(bob), 1e-13) - assert approx(market_amm.get_sum_xy(bob)[1] * 4 // 10, boosted_lm_callback.working_collateral(bob), 1e-13) - update_integral() - elif not is_underwater_bob: - amount_bob = randrange(1, collateral_token.balanceOf(bob) // 10 + 1) - collateral_token.approve(market_controller.address, amount_bob) - max_borrowable_bob = market_controller.max_borrowable(amount_bob + collateral_in_amm_bob, 10, debt_bob) - borrow_amount_bob = min(int(random() * (max_borrowable_bob - debt_bob)), max_borrowable_bob - debt_bob) - if borrow_amount_bob > 0: - print("Bob deposits:", amount_bob, borrow_amount_bob) - if market_controller.loan_exists(bob): - market_controller.borrow_more(amount_bob, borrow_amount_bob) - else: - market_controller.create_loan(amount_bob, borrow_amount_bob, 10) - update_integral() - assert approx(market_amm.get_sum_xy(bob)[1], boosted_lm_callback.user_collateral(bob), 1e-13) - assert approx(market_amm.get_sum_xy(bob)[1] * 4 // 10, boosted_lm_callback.working_collateral(bob), 1e-13) - - # For Alice - if is_alice: - with boa.env.prank(alice): - collateral_in_amm_alice, stablecoin_in_amm_alice, debt_alice, __ = market_controller.user_state(alice) - is_withdraw_alice = (collateral_in_amm_alice > 0) * (random() < 0.5) - is_underwater_alice = stablecoin_in_amm_alice > 0 - - if is_withdraw_alice: - amount_alice = randrange(1, collateral_in_amm_alice + 1) - if amount_alice == collateral_in_amm_alice: - print("Alice repays (full):", debt_alice) - print("Alice withdraws (full):", amount_alice) - market_controller.repay(debt_alice) - assert approx(market_amm.get_sum_xy(alice)[1], boosted_lm_callback.user_collateral(alice), 1e-13) - assert approx(market_amm.get_sum_xy(alice)[1] * 4 // 10, boosted_lm_callback.working_collateral(alice), 1e-13) - elif market_controller.health(alice) > 0: - repay_amount_alice = int(debt_alice // 10 + (debt_alice * 9 // 10) * random() * 0.99) - print("Alice repays:", repay_amount_alice) - market_controller.repay(repay_amount_alice) - if not is_underwater_alice: - min_collateral_required_alice = market_controller.min_collateral(debt_alice - repay_amount_alice, 10) - remove_amount_alice = min(collateral_in_amm_alice - min_collateral_required_alice, amount_alice) - remove_amount_alice = max(remove_amount_alice, 0) - if remove_amount_alice > 0: - print("Alice withdraws:", remove_amount_alice) - market_controller.remove_collateral(remove_amount_alice) - assert approx(market_amm.get_sum_xy(alice)[1], boosted_lm_callback.user_collateral(alice), 1e-13) - assert approx(market_amm.get_sum_xy(alice)[1] * 4 // 10, boosted_lm_callback.working_collateral(alice), 1e-13) - update_integral() - elif not is_underwater_alice: - amount_alice = randrange(1, collateral_token.balanceOf(alice) // 10 + 1) - collateral_token.approve(market_controller.address, amount_alice) - max_borrowable_alice = market_controller.max_borrowable(amount_alice + collateral_in_amm_alice, 10, debt_alice) - borrow_amount_alice = min(int(random() * (max_borrowable_alice - debt_alice)), max_borrowable_alice - debt_alice) - if borrow_amount_alice > 0: - print("Alice deposits:", amount_alice, borrow_amount_alice) - if market_controller.loan_exists(alice): - market_controller.borrow_more(amount_alice, borrow_amount_alice) - else: - market_controller.create_loan(amount_alice, borrow_amount_alice, 10) - update_integral() - assert approx(market_amm.get_sum_xy(alice)[1], boosted_lm_callback.user_collateral(alice), 1e-13) - assert approx(market_amm.get_sum_xy(alice)[1] * 4 // 10, boosted_lm_callback.working_collateral(alice), 1e-13) - - # Chad trading - alice_bands = market_amm.read_user_tick_numbers(alice) - alice_bands = [] if alice_bands[0] == alice_bands[1] else list(range(alice_bands[0], alice_bands[1] + 1)) - bob_bands = market_amm.read_user_tick_numbers(bob) - bob_bands = [] if bob_bands[0] == bob_bands[1] else list(range(bob_bands[0], bob_bands[1] + 1)) - available_bands = alice_bands + bob_bands - print("Bob bands:", bob_bands) - print("Alice bands:", alice_bands) - print("Active band:", market_amm.active_band()) - p_o = market_amm.price_oracle() - upper_bands = sorted(list(filter(lambda band: market_amm.p_oracle_down(band) > p_o, available_bands)))[-5:] - lower_bands = sorted(list(filter(lambda band: market_amm.p_oracle_up(band) < p_o, available_bands)))[:5] - available_bands = upper_bands + lower_bands - if len(available_bands) > 0: - target_band = choice(available_bands) - p_up = market_amm.p_oracle_up(target_band) - p_down = market_amm.p_oracle_down(target_band) - p_target = int(p_down + random() * (p_up - p_down)) - price_oracle.set_price(p_target, sender=admin) - print("Price set to:", p_target) - amount, pump = market_amm.get_amount_for_price(p_target) - with boa.env.prank(chad): - if pump: - market_amm.exchange(0, 1, amount, 0) - else: - market_amm.exchange(1, 0, amount, 0) - print("Swap:", amount, pump) - print("Active band:", market_amm.active_band()) - update_integral() - - # Checking that updating the checkpoint in the same second does nothing - # Also everyone can update: that should make no difference, too - if random() < 0.5: - boosted_lm_callback.user_checkpoint(alice, sender=alice) - if random() < 0.5: - boosted_lm_callback.user_checkpoint(bob, sender=bob) - - dt = randrange(1, YEAR // 20) - boa.env.time_travel(seconds=dt) - print("Time travel", dt) - - total_collateral_from_amm = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() - total_collateral_from_lm_cb = boosted_lm_callback.total_collateral() - working_collateral_from_lm_cb = boosted_lm_callback.working_supply() - print("Total collateral:", total_collateral_from_amm, total_collateral_from_lm_cb) - print("Working collateral:", total_collateral_from_amm * 4 // 10, working_collateral_from_lm_cb) - if total_collateral_from_amm > 0 and total_collateral_from_lm_cb > 0: - assert approx(total_collateral_from_amm, total_collateral_from_lm_cb, 1e-13) - assert approx(total_collateral_from_amm * 4 // 10, working_collateral_from_lm_cb, 1e-13) - - boosted_lm_callback.user_checkpoint(alice, sender=alice) - update_integral() - print(i, dt / 86400, integral, boosted_lm_callback.integrate_fraction(alice), "\n") - assert approx(boosted_lm_callback.integrate_fraction(alice), integral, 1e-14) - - -def test_full_repay_underwater( - accounts, - admin, - chad, - collateral_token, - crv, - boosted_lm_callback, - gauge_controller, - market_controller, - market_amm, - price_oracle, -): - with boa.env.anchor(): - alice, bob = accounts[:2] - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - # Let Alice and Bob have about the same collateral token amount - with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) - - dt = randrange(1, YEAR // 5) - boa.env.time_travel(seconds=dt) - - # Bob creates loan - with boa.env.prank(bob): - amount_bob = 10**20 - collateral_token.approve(market_controller.address, amount_bob) - market_controller.create_loan(amount_bob, int(amount_bob * 2000), 10) - print("Bob deposits:", amount_bob) - - # Alice creates loan - with boa.env.prank(alice): - amount_alice = 10**20 - collateral_token.approve(market_controller.address, amount_alice) - market_controller.create_loan(amount_alice, int(amount_alice * 500), 10) - print("Alice deposits:", amount_alice) - - print(collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y(), - boosted_lm_callback.total_collateral()) - print(boosted_lm_callback.working_supply()) - - # Chad trading. As a result Bob will be underwater - bob_bands = market_amm.read_user_tick_numbers(bob) - bob_bands = list(range(bob_bands[0], bob_bands[1] + 1)) - print("Bob bands:", bob_bands) - print("Active band:", market_amm.active_band()) - target_band = bob_bands[7] - p_up = market_amm.p_oracle_up(target_band) - p_down = market_amm.p_oracle_down(target_band) - p_target = int((p_down + p_up) / 2) - price_oracle.set_price(p_target, sender=admin) - print("Price set to:", p_target) - amount, pump = market_amm.get_amount_for_price(p_target) - with boa.env.prank(chad): - if pump: - market_amm.exchange(0, 1, amount, 0) - else: - market_amm.exchange(1, 0, amount, 0) - print("Swap:", amount, pump, "\n") - print("Active band:", market_amm.active_band()) - - # Bob fully repays being underwater - debt_bob = market_controller.user_state(bob)[2] - market_controller.repay(debt_bob, sender=bob) - print("Bob repays (full):", debt_bob) - print("Bob withdraws (full):", amount_bob) - - total_collateral_from_amm = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() - total_collateral_from_lm_cb = boosted_lm_callback.total_collateral() - working_collateral_from_lm_cb = boosted_lm_callback.working_supply() - print("Total collateral:", total_collateral_from_amm, total_collateral_from_lm_cb) - print("Working collateral:", total_collateral_from_amm * 4 // 10, working_collateral_from_lm_cb) - assert approx(total_collateral_from_amm, total_collateral_from_lm_cb, 1e-15) - assert approx(total_collateral_from_amm * 4 // 10, working_collateral_from_lm_cb, 1e-15) diff --git a/tests/boosted_lm_callback/test_st_as_gauge.py b/tests/boosted_lm_callback/test_st_as_gauge.py deleted file mode 100644 index 3898e34e..00000000 --- a/tests/boosted_lm_callback/test_st_as_gauge.py +++ /dev/null @@ -1,223 +0,0 @@ -import boa -from hypothesis import settings -from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant -from random import random -from ..conftest import approx - - -class StateMachine(RuleBasedStateMachine): - user_id = st.integers(min_value=0, max_value=4) - value = st.integers(min_value=10**16, max_value=10 ** 18 * 10 ** 6 // 3000) - time = st.integers(min_value=300, max_value=86400 * 90) - lock_time = st.integers(min_value=86400 * 7, max_value=86400 * 365 * 4) - - def __init__(self): - super().__init__() - self.checkpoint_supply = 0 - self.checkpoint_working_supply = 0 - self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, - "integral": 0, - "balance": 0, - "working_balance": 0 - } for addr in self.accounts[:5]} - - def update_integrals(self, user, d_balance=0): - # Update rewards - t1 = boa.env.evm.patch.timestamp - t_epoch = self.crv.start_epoch_time_write(sender=self.admin) - rate1 = self.crv.rate() - for acct in self.accounts[:5]: - integral = self.integrals[acct] - if integral["checkpoint"] >= t_epoch: - rate_x_time = (t1 - integral["checkpoint"]) * rate1 - else: - rate_x_time = (t_epoch - integral["checkpoint"]) * self.checkpoint_rate + (t1 - t_epoch) * rate1 - if self.checkpoint_working_supply > 0: - integral["integral"] += rate_x_time * integral["working_balance"] // self.checkpoint_working_supply - integral["checkpoint"] = t1 - if acct == user: - integral["balance"] += d_balance - self.checkpoint_supply += d_balance - self.checkpoint_rate = rate1 - - # Update working balance and supply - voting_balance = self.voting_escrow_delegation_mock.adjusted_balance_of(user) - voting_total = self.voting_escrow.totalSupply() - integral = self.integrals[user] - - lim = integral["balance"] * 40 // 100 - if voting_total > 0: - lim += self.checkpoint_supply * voting_balance // voting_total * 60 // 100 - lim = min(integral["balance"], lim) - - old_bal = integral["working_balance"] - integral["working_balance"] = lim - self.checkpoint_working_supply += lim - old_bal - - @rule(uid=user_id, value=value) - def deposit(self, uid, value): - """ - Make a deposit into the `LiquidityGauge` contract. - - Because of the upper bound of `st_value` relative to the initial account - balances, this rule should never fail. - """ - user = self.accounts[uid] - with boa.env.prank(user): - balance = self.collateral_token.balanceOf(user) - value = min(balance, value) - - if value > 0: - if self.market_controller.loan_exists(user): - self.market_controller.borrow_more(value, int(value * random() * 2000)) - else: - self.market_controller.create_loan(value, int(value * random() * 2000), 10) - self.update_integrals(user, value) - - assert self.collateral_token.balanceOf(user) == balance - value - if self.integrals[user]["integral"] > 0 and self.boosted_lm_callback.integrate_fraction(user) > 0: - assert approx(self.boosted_lm_callback.integrate_fraction(user), self.integrals[user]["integral"], 1e-13) - - @rule(uid=user_id, value=value) - def withdraw(self, uid, value): - """ - Attempt to withdraw from the `LiquidityGauge` contract. - """ - user = self.accounts[uid] - with boa.env.prank(user): - collateral_in_amm, _, debt, __ = self.market_controller.user_state(user) - balance = self.collateral_token.balanceOf(user) - if collateral_in_amm == 0: - return - - if value >= collateral_in_amm: - self.market_controller.repay(debt) - remove_amount = collateral_in_amm - else: - repay_amount = int(debt * random() * 0.99) - self.market_controller.repay(repay_amount) - min_collateral_required = self.market_controller.min_collateral(debt - repay_amount, 10) - remove_amount = min(collateral_in_amm - min_collateral_required, value) - remove_amount = max(remove_amount, 0) - if remove_amount > 0: - self.market_controller.remove_collateral(remove_amount) - self.update_integrals(user, -remove_amount) - - assert self.collateral_token.balanceOf(user) == balance + remove_amount - if self.integrals[user]["integral"] > 0 and self.boosted_lm_callback.integrate_fraction(user) > 0: - assert approx(self.boosted_lm_callback.integrate_fraction(user), self.integrals[user]["integral"], 1e-13) - - @rule(dt=time) - def advance_time(self, dt): - """ - Advance the clock. - """ - boa.env.time_travel(seconds=dt) - - @rule(uid=user_id, dt=lock_time) - def create_lock(self, uid, dt): - """ - Create lock in voting escrow to get boosted. - """ - user = self.accounts[uid] - if self.voting_escrow.locked(user)[0] == 0: - self.voting_escrow.create_lock(10 ** 20, boa.env.evm.patch.timestamp + dt, sender=user) - - @rule(uid=user_id) - def withdraw_from_ve(self, uid): - """ - Withdraw expired lock from voting escrow. - """ - user = self.accounts[uid] - if 0 < self.voting_escrow.locked__end(user) < boa.env.evm.patch.timestamp: - self.voting_escrow.withdraw(sender=user) - - @rule(uid=user_id) - def checkpoint(self, uid): - """ - Create a new user checkpoint. - """ - user = self.accounts[uid] - with boa.env.prank(user): - self.boosted_lm_callback.user_checkpoint(user) - self.update_integrals(user) - if self.integrals[user]["integral"] > 0 and self.boosted_lm_callback.integrate_fraction(user) > 0: - assert approx(self.boosted_lm_callback.integrate_fraction(user), self.integrals[user]["integral"], 1e-13) - - @invariant() - def invariant_collateral(self): - """ - Validate expected balances against actual balances and - expected total supply against actual total supply. - """ - for account, integral in self.integrals.items(): - assert self.boosted_lm_callback.user_collateral(account) == integral["balance"] - assert self.boosted_lm_callback.total_collateral() == sum([i["balance"] for i in self.integrals.values()]) - - @invariant() - def invariant_working_collateral(self): - """ - Validate expected working balances against actual working balances and - expected working supply against actual working supply. - """ - for account, integral in self.integrals.items(): - y1 = self.boosted_lm_callback.working_collateral(account) - y2 = integral["working_balance"] - assert approx(y1, y2, 1e-17) or abs(y1 - y2) < 100 - - Y1 = self.boosted_lm_callback.working_supply() - Y2 = sum([i["working_balance"] for i in self.integrals.values()]) - assert approx(Y1, Y2, 1e-17) or abs(Y1 - Y2) < 10000 - - def teardown(self): - """ - Final check to ensure that all balances may be withdrawn. - """ - for account, integral in ((k, v) for k, v in self.integrals.items() if v): - initial = self.collateral_token.balanceOf(account) - debt = self.market_controller.user_state(account)[2] - self.market_controller.repay(debt, sender=account) - self.update_integrals(account) - - assert not self.market_controller.loan_exists(account) - assert self.collateral_token.balanceOf(account) == initial + integral["balance"] - assert (self.integrals[account]["integral"] > 0) == (self.boosted_lm_callback.integrate_fraction(account) > 0) - if self.integrals[account]["integral"] > 0: - assert approx(self.boosted_lm_callback.integrate_fraction(account), self.integrals[account]["integral"], 1e-13) - - -def test_state_machine( - accounts, - admin, - collateral_token, - crv, - boosted_lm_callback, - gauge_controller, - market_controller, - voting_escrow, - voting_escrow_delegation_mock, -): - for acct in accounts[:5]: - with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) - - with boa.env.prank(acct): - collateral_token.approve(market_controller, 2 ** 256 - 1) - crv.approve(voting_escrow, 2 ** 256 - 1) - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - boa.env.time_travel(seconds=7 * 86400) - - StateMachine.TestCase.settings = settings(max_examples=400, stateful_step_count=50) - for k, v in locals().items(): - setattr(StateMachine, k, v) - run_state_machine_as_test(StateMachine) diff --git a/tests/boosted_lm_callback/test_st_lm_callback.py b/tests/boosted_lm_callback/test_st_lm_callback.py deleted file mode 100644 index e12315cb..00000000 --- a/tests/boosted_lm_callback/test_st_lm_callback.py +++ /dev/null @@ -1,314 +0,0 @@ -import boa -from hypothesis import settings -from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant -from ..conftest import approx - - -class StateMachine(RuleBasedStateMachine): - user_id = st.integers(min_value=0, max_value=4) - deposit_pct = st.floats(min_value=0.001, max_value=1.0) - borrow_pct = st.floats(min_value=0.001, max_value=1.0) - withdraw_pct = st.floats(min_value=0.001, max_value=1.0) - repay_pct = st.floats(min_value=0.001, max_value=1.0) - target_band_pct = st.floats(min_value=0.0, max_value=1.0) - target_price_pct = st.floats(min_value=0.0, max_value=1.0) - time = st.integers(min_value=300, max_value=86400 * 90) - lock_time = st.integers(min_value=86400 * 7, max_value=86400 * 365 * 4) - - def __init__(self): - super().__init__() - self.checkpoint_supply = 0 - self.checkpoint_working_supply = 0 - self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, - "integral": 0, - "balance": 0, - "working_balance": 0, - "boost": 0, - } for addr in self.accounts[:5]} - - def update_integrals(self, user): - # Update rewards - t1 = boa.env.evm.patch.timestamp - t_epoch = self.crv.start_epoch_time_write(sender=self.admin) - rate1 = self.crv.rate() - for acct in self.accounts[:5]: - integral = self.integrals[acct] - if integral["checkpoint"] >= t_epoch: - rate_x_time = (t1 - integral["checkpoint"]) * rate1 - else: - rate_x_time = (t_epoch - integral["checkpoint"]) * self.checkpoint_rate + (t1 - t_epoch) * rate1 - if self.checkpoint_working_supply > 0: - integral["integral"] += rate_x_time * integral["working_balance"] // self.checkpoint_working_supply - integral["checkpoint"] = t1 - integral["balance"] = self.market_amm.get_sum_xy(acct)[1] - self.checkpoint_supply = self.collateral_token.balanceOf(self.market_amm) - self.market_amm.admin_fees_y() - self.checkpoint_rate = rate1 - - # Update working balance and supply - for acct in self.accounts[:5]: - voting_balance = self.voting_escrow_delegation_mock.adjusted_balance_of(acct) - voting_total = self.voting_escrow.totalSupply() - integral = self.integrals[acct] - if acct == user and self.market_controller.user_state(user)[1] == 0: # user is not underwater - lim = integral["balance"] * 40 // 100 - if voting_total > 0: - lim += self.checkpoint_supply * voting_balance // voting_total * 60 // 100 - lim = min(integral["balance"], lim) - - old_bal = integral["working_balance"] - integral["working_balance"] = lim - self.checkpoint_working_supply += lim - old_bal - if integral["balance"] > 0: - integral["boost"] = lim / integral["balance"] - else: - integral["boost"] = 0 - else: - old_bal = integral["working_balance"] - integral["working_balance"] = int(integral["balance"] * integral["boost"]) - self.checkpoint_working_supply += integral["working_balance"] - old_bal - - @rule(uid=user_id, deposit_pct=deposit_pct, borrow_pct=borrow_pct) - def deposit(self, uid, deposit_pct, borrow_pct): - """ - Make a deposit into the `LiquidityGauge` contract. - - Because of the upper bound of `st_value` relative to the initial account - balances, this rule should never fail. - """ - user = self.accounts[uid] - with boa.env.prank(user): - balance = self.collateral_token.balanceOf(user) - deposit_amount = min(int(balance * deposit_pct), balance) - collateral_in_amm, stablecoin_in_amm, debt, __ = self.market_controller.user_state(user) - max_borrowable = self.market_controller.max_borrowable(deposit_amount + collateral_in_amm, 10, debt) - borrow_amount = min(int((max_borrowable - debt) * borrow_pct), max_borrowable - debt) - i = 1 - while True: - try: - self.market_controller.calculate_debt_n1(collateral_in_amm + deposit_amount, debt + borrow_amount, 10) - break - except Exception: - if i == 100: - break - i += 1 - borrow_amount = borrow_amount * (100 - i) // 100 - is_underwater = stablecoin_in_amm > 0 - if borrow_amount <= 0 or is_underwater: - return - - if self.market_controller.loan_exists(user): - self.market_controller.borrow_more(deposit_amount, borrow_amount) - else: - self.market_controller.create_loan(deposit_amount, borrow_amount, 10) - assert self.collateral_token.balanceOf(user) == balance - deposit_amount - - self.update_integrals(user) - r1 = self.boosted_lm_callback.integrate_fraction(user) - r2 = self.integrals[user]["integral"] - assert (r1 > 0) == (r2 > 0) - if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 - - @rule(uid=user_id, withdraw_pct=withdraw_pct, repay_pct=repay_pct) - def withdraw(self, uid, withdraw_pct, repay_pct): - """ - Attempt to withdraw from the `LiquidityGauge` contract. - """ - user = self.accounts[uid] - with boa.env.prank(user): - balance = self.collateral_token.balanceOf(user) - collateral_in_amm, stablecoin_in_amm, debt, __ = self.market_controller.user_state(user) - is_underwater = stablecoin_in_amm > 0 - if collateral_in_amm == 0: - return - - withdraw_amount = 0 - if repay_pct == 1: - self.market_controller.repay(debt) - withdraw_amount = collateral_in_amm - elif self.market_controller.health(user) > 0: - repay_amount = int(debt * repay_pct) - self.market_controller.repay(repay_amount) - if is_underwater: - # Underwater repay does not trigger callback, so we call checkpoint manually to pass checks below - self.boosted_lm_callback.user_checkpoint(user) - else: - withdraw_amount = int(collateral_in_amm * withdraw_pct) - min_collateral_required = self.market_controller.min_collateral(debt - repay_amount, 10) - withdraw_amount = min(collateral_in_amm - min_collateral_required, withdraw_amount) * 99 // 100 - withdraw_amount = max(withdraw_amount, 0) - if withdraw_amount > 0: - self.market_controller.remove_collateral(withdraw_amount) - else: - # We call checkpoint manually to pass checks below - self.boosted_lm_callback.user_checkpoint(user) - self.update_integrals(user) - - assert self.collateral_token.balanceOf(user) == balance + withdraw_amount - - r1 = self.boosted_lm_callback.integrate_fraction(user) - r2 = self.integrals[user]["integral"] - assert (r1 > 0) == (r2 > 0) - if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 - - @rule(target_band_pct=target_band_pct, target_price_pct=target_price_pct) - def trade(self, target_band_pct, target_price_pct): - """ - Makes a trade in LLAMMA. - """ - with boa.env.prank(self.chad): - available_bands = [] - for acct in self.accounts[:5]: - border_bands = self.market_amm.read_user_tick_numbers(acct) - available_bands += [] if border_bands[0] == border_bands[1] else list(range(border_bands[0], border_bands[1] + 1)) - p_o = self.market_amm.price_oracle() - upper_bands = sorted(list(filter(lambda band: self.market_amm.p_oracle_down(band) > p_o, available_bands)))[-5:] - lower_bands = sorted(list(filter(lambda band: self.market_amm.p_oracle_up(band) < p_o, available_bands)))[:5] - available_bands = upper_bands + lower_bands - if len(available_bands) > 0: - target_band = available_bands[int(target_band_pct * (len(available_bands) - 1))] - p_up = self.market_amm.p_oracle_up(target_band) - p_down = self.market_amm.p_oracle_down(target_band) - p_target = int(p_down + target_price_pct * (p_up - p_down)) - self.price_oracle.set_price(p_target, sender=self.admin) - amount, pump = self.market_amm.get_amount_for_price(p_target) - balance = self.stablecoin.balanceOf(self.chad) if pump else self.collateral_token.balanceOf(self.chad) - amount = min(amount, balance) - if amount > 0: - if pump: - self.market_amm.exchange(0, 1, amount, 0) - else: - self.market_amm.exchange(1, 0, amount, 0) - self.update_integrals(None) - - @rule(dt=time) - def advance_time(self, dt): - """ - Advance the clock. - """ - boa.env.time_travel(seconds=dt) - - @rule(uid=user_id, dt=lock_time) - def create_lock(self, uid, dt): - """ - Create lock in voting escrow to get boosted. - """ - user = self.accounts[uid] - if self.voting_escrow.locked(user)[0] == 0: - self.voting_escrow.create_lock(10 ** 20, boa.env.evm.patch.timestamp + dt, sender=user) - - @rule(uid=user_id) - def withdraw_from_ve(self, uid): - """ - Withdraw expired lock from voting escrow. - """ - user = self.accounts[uid] - if 0 < self.voting_escrow.locked__end(user) < boa.env.evm.patch.timestamp: - self.voting_escrow.withdraw(sender=user) - - @rule(uid=user_id) - def checkpoint(self, uid): - """ - Create a new user checkpoint. - """ - user = self.accounts[uid] - with boa.env.prank(user): - self.boosted_lm_callback.user_checkpoint(user) - self.update_integrals(user) - r1 = self.boosted_lm_callback.integrate_fraction(user) - r2 = self.integrals[user]["integral"] - assert (r1 > 0) == (r2 > 0) - if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 - - @invariant() - def invariant_collateral(self): - """ - Validate expected balances against actual balances and - expected total supply against actual total supply. - """ - for account, integral in self.integrals.items(): - y1 = self.boosted_lm_callback.user_collateral(account) - y2 = integral["balance"] - assert approx(y1, y2, 1e-14) or abs(y1 - y2) < 100000 # Seems ok for 18 decimals - - Y1 = self.boosted_lm_callback.total_collateral() - Y2 = sum([i["balance"] for i in self.integrals.values()]) - assert approx(Y1, Y2, 1e-13) or abs(Y1 - Y2) < 100000 # Seems ok for 18 decimals - - @invariant() - def invariant_working_collateral(self): - """ - Validate expected working balances against actual working balances and - expected working supply against actual working supply. - """ - for account, integral in self.integrals.items(): - y1 = self.boosted_lm_callback.working_collateral(account) - y2 = integral["working_balance"] - assert approx(y1, y2, 1e-14) or abs(y1 - y2) < 100000 # Seems ok for 18 decimals - - Y1 = self.boosted_lm_callback.working_supply() - Y2 = sum([i["working_balance"] for i in self.integrals.values()]) - assert approx(Y1, Y2, 1e-13) or abs(Y1 - Y2) < 100000 # Seems ok for 18 decimals - - def teardown(self): - """ - Final check to ensure that all balances may be withdrawn. - """ - for account, integral in ((k, v) for k, v in self.integrals.items() if v): - initial_balance = self.collateral_token.balanceOf(account) - collateral_in_amm = integral["balance"] - debt = self.market_controller.user_state(account)[2] - self.market_controller.repay(debt, sender=account) - self.update_integrals(account) - - assert not self.market_controller.loan_exists(account) - assert self.collateral_token.balanceOf(account) == initial_balance + collateral_in_amm - - r1 = self.boosted_lm_callback.integrate_fraction(account) - r2 = self.integrals[account]["integral"] - assert (r1 > 0) == (r2 > 0) - if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 - - -def test_state_machine( - accounts, - admin, - chad, - stablecoin, - collateral_token, - crv, - boosted_lm_callback, - gauge_controller, - market_controller, - market_amm, - price_oracle, - voting_escrow, - voting_escrow_delegation_mock, -): - for acct in accounts[:5]: - with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) - - with boa.env.prank(acct): - collateral_token.approve(market_controller, 2 ** 256 - 1) - crv.approve(voting_escrow, 2 ** 256 - 1) - - # Wire up Gauge to the controller to have proper rates and stuff - with boa.env.prank(admin): - gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) - gauge_controller.add_gauge(boosted_lm_callback.address, 0, 10 ** 18) - - boa.env.time_travel(seconds=7 * 86400) - - StateMachine.TestCase.settings = settings(max_examples=400, stateful_step_count=50) - for k, v in locals().items(): - setattr(StateMachine, k, v) - run_state_machine_as_test(StateMachine) diff --git a/tests/conftest.py b/tests/conftest.py index 004dc28e..36e37534 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,76 +1,288 @@ -import os from datetime import timedelta -from math import log -from typing import Any, Callable import boa import pytest -from hypothesis import settings - +from hypothesis import settings, Phase +from tests.utils.deployers import ( + ERC20_MOCK_DEPLOYER, + CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + FAKE_LEVERAGE_DEPLOYER, + DUMMY_CALLBACK_DEPLOYER, +) +from tests.utils.protocols import Llamalend boa.env.enable_fast_mode() -PRICE = 3000 +TESTING_DECIMALS = [2, 6, 8, 9, 18] -settings.register_profile("default", deadline=timedelta(seconds=1000)) -settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) +no_shrink = settings.register_profile( + "no-shrink", + phases=list(Phase)[:4], + deadline=timedelta(seconds=1000), + print_blob=True, +) +settings.register_profile( + "quick", + parent=no_shrink, + max_examples=3, + stateful_step_count=15, +) +settings.load_profile("no-shrink") -def approx(x1: float, x2: float, precision: float, abs_precision=None): - if precision >= 1: - return True - result = False - if abs_precision is not None: - result = abs(x2 - x1) <= abs_precision - else: - abs_precision = 0 - if x2 == 0: - return abs(x1) <= abs_precision - elif x1 == 0: - return abs(x2) <= abs_precision - return result or (abs(log(x1 / x2)) <= precision) +@pytest.fixture(scope="module") +def proto(): + return Llamalend() -@pytest.fixture(scope="session") -def accounts(): - return [boa.env.generate_address() for _ in range(10)] +@pytest.fixture(scope="module") +def admin(proto): + return proto.admin -@pytest.fixture(scope="session") -def admin(): - return boa.env.generate_address() +@pytest.fixture(scope="module") +def factory(proto): + return proto.lending_factory -@pytest.fixture(scope="session") -def token_mock(): - return boa.load_partial('contracts/testing/ERC20Mock.vy') +@pytest.fixture(scope="module") +def stablecoin(proto): + return proto.crvUSD -@pytest.fixture(scope="session") -def get_collateral_token(token_mock, admin) -> Callable[[int], Any]: - def f(digits): - with boa.env.prank(admin): - return token_mock.deploy("Colalteral", "ETH", digits) - return f +@pytest.fixture(scope="module") +def collateral_token(): + # TODO hook decimals fixture + return ERC20_MOCK_DEPLOYER.deploy(18) -@pytest.fixture(scope="session") -def get_borrowed_token(token_mock, admin) -> Callable[[int], Any]: - def f(digits): + +@pytest.fixture(scope="module") +def borrowed_token(stablecoin): + """Default borrowed token for lending tests (crvUSD). + Specific test modules can override this if needed. + """ + # TODO should parametrize to use other tokens in lending + return stablecoin + + +@pytest.fixture(scope="module") +def fake_leverage(controller, collateral_token, borrowed_token, price_oracle): + market_price = price_oracle.price() + leverage = FAKE_LEVERAGE_DEPLOYER.deploy( + borrowed_token.address, + collateral_token.address, + controller.address, + market_price, + ) + return leverage + + +@pytest.fixture(scope="module") +def dummy_callback(): + return DUMMY_CALLBACK_DEPLOYER.deploy() + + +@pytest.fixture(scope="module") +def mint_monetary_policy(proto): + return proto.mint_monetary_policy + + +@pytest.fixture(scope="module") +def lending_monetary_policy(): + """Default monetary policy deployer for lending markets (override in suites).""" + return CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER + + +@pytest.fixture(scope="module") +def monetary_policy( + market_type, controller, mint_monetary_policy, lending_monetary_policy +): + """Actual monetary policy contract bound to this market (post-creation).""" + if market_type == "mint": + return mint_monetary_policy + return lending_monetary_policy.at(controller.monetary_policy()) + + +@pytest.fixture(scope="module") +def price_oracle(proto): + return proto.price_oracle + + +@pytest.fixture(scope="module") +def amm_impl(proto): + return proto.blueprints.amm + + +@pytest.fixture(scope="module") +def controller_impl(proto): + return proto.blueprints.lend_controller + + +@pytest.fixture(scope="module") +def vault_impl(proto): + return proto.vault_impl + + +@pytest.fixture(scope="module") +def price_oracle_impl(proto): + return proto.blueprints.price_oracle + + +@pytest.fixture(scope="module") +def mpolicy_impl(proto): + return proto.blueprints.mpolicy + + +@pytest.fixture(scope="module", params=["mint", "lending"]) +def market_type(request): + return request.param + + +@pytest.fixture(scope="module") +def amm_A(): + return 100 + + +@pytest.fixture(scope="module") +def amm_fee(): + return 10**16 + + +@pytest.fixture(scope="module") +def loan_discount(): + return int(0.09 * 10**18) + + +@pytest.fixture(scope="module") +def liquidation_discount(): + return int(0.06 * 10**18) + + +@pytest.fixture(scope="module") +def min_borrow_rate(): + return 10**15 // (365 * 86400) # 0.1% APR + + +@pytest.fixture(scope="module") +def max_borrow_rate(): + return 10**18 // (365 * 86400) # 100% APR + + +@pytest.fixture(scope="module") +def market( + market_type, + proto, + collateral_token, + price_oracle, + seed_liquidity, + borrowed_token, + mint_monetary_policy, + lending_monetary_policy, + amm_A, + amm_fee, + loan_discount, + liquidation_discount, + min_borrow_rate, + max_borrow_rate, +): + if market_type == "mint": + return proto.create_mint_market( + collateral_token=collateral_token, + price_oracle=price_oracle, + monetary_policy=mint_monetary_policy, + A=amm_A, + amm_fee=amm_fee, + loan_discount=loan_discount, + liquidation_discount=liquidation_discount, + debt_ceiling=seed_liquidity, + ) + elif market_type == "lending": + return proto.create_lending_market( + borrowed_token=borrowed_token, + collateral_token=collateral_token, + A=amm_A, + fee=amm_fee, + loan_discount=loan_discount, + liquidation_discount=liquidation_discount, + price_oracle=price_oracle, + name="Test Vault", + min_borrow_rate=min_borrow_rate, + max_borrow_rate=max_borrow_rate, + seed_amount=seed_liquidity, + mpolicy_deployer=lending_monetary_policy, + ) + else: + raise ValueError("Incorrect market type fixture") + + +@pytest.fixture(scope="module") +def controller(market, market_type, admin, borrow_cap): + """Controller for the current market (mint or lending). + Sets borrow cap for lending markets to `borrow_cap`. + """ + ctrl = market["controller"] + if market_type == "lending" and borrow_cap is not None: with boa.env.prank(admin): - return token_mock.deploy("Rugworks USD", "rUSD", digits) - return f + ctrl.set_borrow_cap(borrow_cap) + return ctrl + + +@pytest.fixture(scope="module") +def amm(market): + """AMM for the current market (mint or lending).""" + return market["amm"] + + +@pytest.fixture(scope="module") +def vault(market, market_type): + """Vault for the current market (mint or lending). + Mint markets do not have a vault; return None in that case. + """ + if market_type == "lending": + return market["vault"] + return None + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 1000 * 10**18 + + +@pytest.fixture(scope="module") +def borrow_cap(seed_liquidity): + """Default borrow cap for lending markets equals `seed_liquidity`. + Override to adjust or set to None to skip setting. Ignored for mint. + """ + return seed_liquidity + + +# ============== Account Fixtures ============== @pytest.fixture(scope="session") -def collateral_token(get_collateral_token): - return get_collateral_token(18) +def accounts(): + return [boa.env.generate_address() for _ in range(10)] + + +@pytest.fixture(scope="module") +def alice(): + return boa.env.generate_address("alice") + + +# ============== Token Fixtures ============== + + +@pytest.fixture(scope="module", params=TESTING_DECIMALS) +def decimals(request): + return request.param @pytest.fixture(scope="session") -def price_oracle(admin): - with boa.env.prank(admin): - oracle = boa.load('contracts/testing/DummyPriceOracle.vy', admin, PRICE * 10**18) - return oracle +def token_mock(): + return ERC20_MOCK_DEPLOYER diff --git a/tests/controller/conftest.py b/tests/controller/conftest.py new file mode 100644 index 00000000..d02b6dfa --- /dev/null +++ b/tests/controller/conftest.py @@ -0,0 +1,19 @@ +from pytest import fixture +from tests.utils.deployers import ERC20_MOCK_DEPLOYER + +# Common fixtures (proto, admin) are now in tests/conftest.py + + +@fixture(scope="module", params=[2, 6, 8, 9, 18]) +def decimals(request): + return request.param + + +@fixture(scope="module") +def collat(decimals): + return ERC20_MOCK_DEPLOYER.deploy(decimals) + + +@fixture(scope="module") +def borrow(decimals): + return ERC20_MOCK_DEPLOYER.deploy(decimals) diff --git a/tests/controller/test_packing.py b/tests/controller/test_packing.py new file mode 100644 index 00000000..0c18cc00 --- /dev/null +++ b/tests/controller/test_packing.py @@ -0,0 +1,36 @@ +import boa +from hypothesis import given +from hypothesis import strategies as st + +from tests.utils.constants import MAX_TICKS + +MAX_N = 2**127 - 1 +MIN_N = -(2**127) + 1 # <- not -2**127! +MAX_SPAN = MAX_TICKS - 1 +DEPOSIT_AMOUNT = 10**6 +MAX_SKIP_TICKS = 1024 + + +@st.composite +def tick_ranges(draw, active_band): + min_n1 = max(MIN_N, active_band - (MAX_SKIP_TICKS - 1)) + max_n1 = min(MAX_N, active_band + MAX_SKIP_TICKS) + n1 = draw(st.integers(min_value=min_n1, max_value=max_n1)) + max_n2 = min(MAX_N, n1 + MAX_SPAN) + n2 = draw(st.integers(min_value=n1, max_value=max_n2)) + return n1, n2 + + +@given(data=st.data()) +def test_ammpack_round_trip(amm, controller, data): + active_band = amm.active_band() + n1, n2 = data.draw(tick_ranges(active_band=active_band)) + + with boa.env.anchor(): + amm.deposit_range( + boa.env.eoa, DEPOSIT_AMOUNT, n1, n2, sender=controller.address + ) + + n1out, n2out = amm.read_user_tick_numbers(boa.env.eoa) + assert n1out == n1 + assert n2out == n2 diff --git a/tests/controller/test_set_price_oracle.py b/tests/controller/test_set_price_oracle.py new file mode 100644 index 00000000..21821185 --- /dev/null +++ b/tests/controller/test_set_price_oracle.py @@ -0,0 +1,170 @@ +import pytest +import boa +from tests.utils.deployers import ( + DUMMY_PRICE_ORACLE_DEPLOYER, + ERC20_MOCK_DEPLOYER, +) +from tests.utils.constants import MAX_UINT256, MAX_ORACLE_PRICE_DEVIATION + + +@pytest.fixture(scope="module") +def decimals(): + # Overrides global fixture as these tests don't + # change behavior with different decimals + return 18 + + +@pytest.fixture(scope="module") +def new_oracle(admin): + """Deploy a new price oracle for testing.""" + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, 3000 * 10**18, sender=admin) + + +def test_default_behavior(controller, amm, new_oracle, admin): + """Test normal oracle update with valid parameters.""" + initial_oracle = amm.price_oracle_contract() + assert initial_oracle != new_oracle + + # Set new oracle with reasonable max deviation + max_deviation = 10**17 # 10% + controller.set_price_oracle(new_oracle, max_deviation, sender=admin) + + # Verify oracle was updated on AMM + assert amm.price_oracle_contract() == new_oracle.address + + +def test_admin_access_control(controller, new_oracle): + """Test that only admin can call set_price_oracle.""" + max_deviation = 10**17 # 10% + + with boa.reverts("only admin"): + controller.set_price_oracle(new_oracle, max_deviation) + + +def test_max_deviation_validation_too_high(controller, new_oracle, admin): + """Test that max_deviation cannot exceed MAX_ORACLE_PRICE_DEVIATION.""" + # MAX_ORACLE_PRICE_DEVIATION is 50% (WAD // 2) + invalid_deviation = MAX_ORACLE_PRICE_DEVIATION + 1 + + with boa.reverts(dev="invalid max deviation"): + controller.set_price_oracle(new_oracle, invalid_deviation, sender=admin) + + +def test_max_deviation_validation_boundary(controller, new_oracle, admin, amm): + """Test that exactly MAX_ORACLE_PRICE_DEVIATION is accepted.""" + # Should succeed at boundary + controller.set_price_oracle(new_oracle, MAX_ORACLE_PRICE_DEVIATION, sender=admin) + assert amm.price_oracle_contract() == new_oracle.address + + +@pytest.fixture(scope="module") +def high_deviation_oracle(admin): + """Deploy an oracle with high price deviation for testing.""" + # 60% higher price - exceeds MAX_ORACLE_PRICE_DEVIATION + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, 4800 * 10**18, sender=admin) + + +def test_max_deviation_skip_check(controller, high_deviation_oracle, admin, amm, proto): + """Test that max_value(uint256) skips deviation check.""" + # Verify high_deviation_oracle is ~60% higher than initial oracle + initial_price = proto.price_oracle.price() + high_price = high_deviation_oracle.price() + expected_price = initial_price * 160 // 100 # 60% higher + assert ( + abs(high_price - expected_price) < initial_price // 100 + ) # Within 1% tolerance + + # Even with high price deviation, should succeed when max_deviation is max_value + controller.set_price_oracle(high_deviation_oracle, MAX_UINT256, sender=admin) + assert amm.price_oracle_contract() == high_deviation_oracle.address + + +@pytest.fixture(scope="module") +def broken_oracle(): + """Deploy a broken oracle without required methods.""" + # This is just a plain ERC20 that doesn't have price() or price_w() methods + return ERC20_MOCK_DEPLOYER.deploy(18) + + +def test_oracle_validation_missing_methods(controller, broken_oracle, admin): + """Test that oracle without required methods reverts.""" + max_deviation = 10**17 # 10% + + # Should revert when trying to call price_w() on broken oracle + with boa.reverts(): + controller.set_price_oracle(broken_oracle, max_deviation, sender=admin) + + +@pytest.fixture(scope="module") +def different_price_oracle(admin): + """Deploy an oracle with a different price for testing price deviation.""" + # 10% higher price + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, 3300 * 10**18, sender=admin) + + +def test_price_deviation_check_within_limit( + controller, different_price_oracle, admin, amm +): + """Test successful update when price deviation is within limit.""" + # 10% price difference, 20% max deviation allowed + max_deviation = 2 * 10**17 # 20% + + controller.set_price_oracle(different_price_oracle, max_deviation, sender=admin) + assert amm.price_oracle_contract() == different_price_oracle.address + + +def test_price_deviation_check_exceeds_limit(controller, different_price_oracle, admin): + """Test that update fails when price deviation exceeds limit.""" + # 10% price difference, but only 5% max deviation allowed + max_deviation = 5 * 10**16 # 5% + + with boa.reverts("delta>max"): + controller.set_price_oracle(different_price_oracle, max_deviation, sender=admin) + + +def test_price_deviation_calculation_higher_new_price(controller, admin, amm): + """Test deviation calculation when new price is higher than old.""" + # Create oracle with 15% higher price + higher_price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, 3450 * 10**18, sender=admin + ) + + # Should succeed with 20% max deviation + controller.set_price_oracle(higher_price_oracle, 2 * 10**17, sender=admin) + assert amm.price_oracle_contract() == higher_price_oracle.address + + +def test_price_deviation_calculation_lower_new_price(controller, admin, amm): + """Test deviation calculation when new price is lower than old.""" + # Create oracle with 15% lower price + lower_price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, 2550 * 10**18, sender=admin + ) + + # Should succeed with 20% max deviation + controller.set_price_oracle(lower_price_oracle, 2 * 10**17, sender=admin) + assert amm.price_oracle_contract() == lower_price_oracle.address + + +def test_price_deviation_at_exact_limit(controller, admin, amm): + """Test oracle update at exact deviation limit.""" + # Create oracle with exactly 10% higher price + exact_limit_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, 3300 * 10**18, sender=admin + ) + + # Should succeed with exactly 10% max deviation + controller.set_price_oracle(exact_limit_oracle, 10**17, sender=admin) + assert amm.price_oracle_contract() == exact_limit_oracle.address + + +def test_same_price_different_oracle(controller, admin, amm): + """Test updating to a new oracle with the same price.""" + # Create oracle with same price as initial + same_price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, 3000 * 10**18, sender=admin + ) + + # Should succeed even with 0 deviation allowed + controller.set_price_oracle(same_price_oracle, 0, sender=admin) + assert amm.price_oracle_contract() == same_price_oracle.address diff --git a/tests/crosshair/deposited_withdrawn.py b/tests/crosshair/deposited_withdrawn.py new file mode 100644 index 00000000..23b01b99 --- /dev/null +++ b/tests/crosshair/deposited_withdrawn.py @@ -0,0 +1,26 @@ +from z3 import Int, Solver + +solver = Solver() + + +def Uint256(name: str): + x = Int(name) + solver.add(x >= 0, x < 2**256) + return x + + +def assert_not_Uint256(x: Int): + solver.add((x < 0) | (x >= 2**256)) + + +# vault_deposited = Uint256("vault_deposited") +# vault_withdrawn = Uint256("vault_withdrawn") + + +# assert_not_Uint256(vault_deposited - vault_withdrawn) + +# res = solver.check() + +# if res == sat: +# from pprint import pprint +# pprint(solver.model()) diff --git a/tests/e2e/test_borrow_caps.py b/tests/e2e/test_borrow_caps.py new file mode 100644 index 00000000..cb446114 --- /dev/null +++ b/tests/e2e/test_borrow_caps.py @@ -0,0 +1,80 @@ +import boa +import pytest + +from tests.utils.constants import MAX_UINT256 + +COLLATERAL = 10**17 +EXTRA_COLLATERAL = 10**17 +PREMINT_COLLATERAL = 10**24 +PREMINT_BORROWED = 10**24 +N_BANDS = 5 + + +@pytest.fixture(scope="module") +def market_type(): + # Borrow caps don't apply to mint markets + return "lending" + + +@pytest.fixture(scope="module") +def borrow_cap(): + # Start with zero cap to recreate the conditions of a freshly deployed market + return 0 + + +def test_borrow_cap(controller, admin, collateral_token, borrowed_token): + # Pre-mint ample balances and approvals for the default EOA + boa.deal(collateral_token, boa.env.eoa, PREMINT_COLLATERAL) + boa.deal(borrowed_token, boa.env.eoa, PREMINT_BORROWED) + collateral_token.approve(controller, MAX_UINT256) + borrowed_token.approve(controller, MAX_UINT256) + + # Borrow cap is zero at deployment; any loan should revert + assert controller.borrow_cap() == 0 + with boa.reverts("Borrow cap exceeded"): + controller.create_loan(COLLATERAL, 1, N_BANDS) + + # Raise the cap modestly and open a loan that consumes the allowance + debt_cap = 1 + controller.set_borrow_cap(debt_cap, sender=admin) + assert controller.available_balance() > 0 + controller.create_loan(COLLATERAL, debt_cap, N_BANDS) + assert controller.available_balance() > 0 + assert controller.total_debt() == debt_cap + + # Attempts to borrow beyond the cap should revert + with boa.reverts("Borrow cap exceeded"): + controller.borrow_more(0, 1) + + # Increase collateral, temporarily lift the cap, and borrow more within the new limit + controller.add_collateral(EXTRA_COLLATERAL) + controller.set_borrow_cap(MAX_UINT256, sender=admin) + collateral_locked, _, current_debt, bands = controller.user_state(boa.env.eoa) + max_total = controller.max_borrowable( + collateral_locked, bands, current_debt, boa.env.eoa + ) + extra_debt = max_total - current_debt + assert extra_debt > 0 + controller.set_borrow_cap(current_debt + extra_debt, sender=admin) + assert controller.available_balance() > 0 + controller.borrow_more(0, extra_debt) + assert controller.available_balance() > 0 + assert controller.total_debt() == current_debt + extra_debt + + # Cutting the cap back to zero blocks further borrowing but allows repayments + controller.set_borrow_cap(0, sender=admin) + with boa.reverts("Borrow cap exceeded"): + controller.borrow_more(0, 1) + + # Repay the full position and exit cleanly + assert controller.available_balance() > 0 + controller.repay(MAX_UINT256) + assert controller.available_balance() > 0 + remaining_collateral, _, debt_after_repay, _ = controller.user_state(boa.env.eoa) + assert debt_after_repay == 0 + if remaining_collateral > 0: + controller.remove_collateral(remaining_collateral) + + # With a zero cap, opening a fresh loan remains disallowed + with boa.reverts("Borrow cap exceeded"): + controller.create_loan(COLLATERAL, 1, N_BANDS) diff --git a/tests/e2e/test_donate_dos.py b/tests/e2e/test_donate_dos.py new file mode 100644 index 00000000..975cc32f --- /dev/null +++ b/tests/e2e/test_donate_dos.py @@ -0,0 +1,39 @@ +import boa +import pytest + +from tests.utils.constants import MAX_UINT256 + + +@pytest.fixture(scope="module") +def market_type(): + return "lending" + + +@pytest.fixture(scope="module") +def borrow_cap(): + return MAX_UINT256 + + +def test_reverts_when_lent_exceeds_deposited_under_donation( + controller, vault, borrowed_token, collateral_token +): + """ + Test that verifies that issue: CS-CRVUSD-094 is fixed + as the pool doesn't allow donated tokens to be lent out. + """ + initial_balance = vault.net_deposits() + + COLLATERAL = 10**30 + N_BANDS = 5 + DONATION = 1 + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + collateral_token.approve(controller, MAX_UINT256) + + boa.deal(borrowed_token, boa.env.eoa, DONATION) + borrowed_token.transfer(controller, DONATION) + + with boa.reverts("Borrowed balance exceeded"): + controller.create_loan(COLLATERAL, initial_balance + DONATION, N_BANDS) + + vault.totalAssets() diff --git a/tests/e2e/test_resupply_hack.py b/tests/e2e/test_resupply_hack.py new file mode 100644 index 00000000..8d387763 --- /dev/null +++ b/tests/e2e/test_resupply_hack.py @@ -0,0 +1,45 @@ +import boa +import pytest + +from tests.utils import max_approve + +# Amounts used by the attacker during the hack +DEPOSIT = 2_000_000_000_000_000_001 +DONATION = 2_000 * 10**18 + + +@pytest.fixture(scope="module") +def market_type(): + """The attach was performed on a lending market (as mint markets don't have vaults).""" + return "lending" + + +def test_vault_inflation(controller, vault, borrowed_token): + """ + Historically, vault donations could inflate `pricePerShare()` because the vault + relied on the controller's live token balance instead of an internally tracked + one. This quirk was safe for LlamaLend itself, yet it complicated integrations + that plug the shares into external solvency or pricing logic. The vault now keeps + internal accounting, so external top-ups can no longer affect later deposits and + `pricePerShare()` stays stable, making downstream integrations easier to reason about. + See the resupply hack postmortem for more details: + https://mirror.xyz/0x521CB9b35514E9c8a8a929C890bf1489F63B2C84/ygJ1kh6satW9l_NDBM47V87CfaQbn2q0tWy_rtp76OI + """ + max_approve(borrowed_token, vault) + boa.deal(borrowed_token, boa.env.eoa, DEPOSIT) + + # Baseline without any donation + with boa.env.anchor(): + vault.deposit(DEPOSIT) + pps_without_donation = vault.pricePerShare() + + # Prepare balances and approvals for the real scenario + boa.deal(borrowed_token, boa.env.eoa, DEPOSIT + DONATION) + + # Donation to the controller followed by the first valid deposit + borrowed_token.transfer(controller, DONATION) + vault.deposit(DEPOSIT) + + pps_with_donation = vault.pricePerShare() + + assert pps_with_donation == pps_without_donation diff --git a/tests/flashloan/conftest.py b/tests/flashloan/conftest.py index 70946669..d7c8cff5 100644 --- a/tests/flashloan/conftest.py +++ b/tests/flashloan/conftest.py @@ -1,87 +1,27 @@ import boa import pytest - - -@pytest.fixture(scope="session") -def stablecoin_pre(): - return boa.load_partial('contracts/Stablecoin.vy') +from tests.utils.deployers import FLASH_LENDER_DEPLOYER, DUMMY_FLASH_BORROWER_DEPLOYER @pytest.fixture(scope="module") -def stablecoin(stablecoin_pre, admin): - with boa.env.prank(admin): - _stablecoin = stablecoin_pre.deploy('Curve USD', 'crvUSD') - _stablecoin.mint(admin, 10**21) - return _stablecoin - - -@pytest.fixture(scope="session") -def weth(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/WETH.vy') - - -@pytest.fixture(scope="session") -def controller_factory_impl(): - return boa.load_partial('contracts/ControllerFactory.vy') - - -@pytest.fixture(scope="module") -def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, accounts): - with boa.env.prank(admin): - return controller_factory_impl.deploy(stablecoin.address, admin, accounts[0], weth.address) - - -@pytest.fixture(scope="session") -def controller_interface(): - return boa.load_partial('contracts/flashloan/FlashLender.vy') - - -@pytest.fixture(scope="session") -def controller_impl(controller_interface, admin): - with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def amm_interface(): - return boa.load_partial('contracts/AMM.vy') - - -@pytest.fixture(scope="session") -def amm_impl(amm_interface, admin): - with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def user(): - return boa.env.generate_address() - - -@pytest.fixture(scope="module") -def controller_factory(controller_prefactory, amm_impl, controller_impl, stablecoin, admin): - with boa.env.prank(admin): - controller_prefactory.set_implementations(controller_impl.address, amm_impl.address) - stablecoin.set_minter(controller_prefactory.address) - return controller_prefactory +def controller_factory(proto): + return proto.mint_factory @pytest.fixture(scope="module") def max_flash_loan(): - return 3 * 10**6 * 10 ** 18 + return 3 * 10**6 * 10**18 @pytest.fixture(scope="module") def flash_lender(controller_factory, admin, max_flash_loan): with boa.env.prank(admin): - fl = boa.load('contracts/flashloan/FlashLender.vy', controller_factory.address) + fl = FLASH_LENDER_DEPLOYER.deploy(controller_factory.address) controller_factory.set_debt_ceiling(fl.address, max_flash_loan) - return fl @pytest.fixture(scope="module") def flash_borrower(flash_lender, admin): with boa.env.prank(admin): - return boa.load('contracts/testing/DummyFlashBorrower.vy', flash_lender.address) + return DUMMY_FLASH_BORROWER_DEPLOYER.deploy(flash_lender.address) diff --git a/tests/flashloan/test_debt_ceiling.py b/tests/flashloan/test_debt_ceiling.py index dc135ed2..85ff113b 100644 --- a/tests/flashloan/test_debt_ceiling.py +++ b/tests/flashloan/test_debt_ceiling.py @@ -1,7 +1,9 @@ import boa -def test_increase_debt_ceiling(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): +def test_increase_debt_ceiling( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan assert stablecoin.balanceOf(flash_lender) == max_flash_loan @@ -11,7 +13,9 @@ def test_increase_debt_ceiling(controller_factory, flash_lender, stablecoin, max assert stablecoin.balanceOf(flash_lender) == max_flash_loan * 2 -def test_decrease_debt_ceiling(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): +def test_decrease_debt_ceiling( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan assert stablecoin.balanceOf(flash_lender) == max_flash_loan @@ -21,7 +25,9 @@ def test_decrease_debt_ceiling(controller_factory, flash_lender, stablecoin, max assert stablecoin.balanceOf(flash_lender) == max_flash_loan // 2 -def test_empty_flash_lender(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): +def test_empty_flash_lender( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan assert stablecoin.balanceOf(flash_lender) == max_flash_loan @@ -31,40 +37,43 @@ def test_empty_flash_lender(controller_factory, flash_lender, stablecoin, max_fl assert stablecoin.balanceOf(flash_lender) == 0 -def test_increase_debt_ceiling_with_excess(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): - with boa.env.prank(admin): - stablecoin.transfer(flash_lender, 10**21) +def test_increase_debt_ceiling_with_excess( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): + boa.deal(stablecoin, flash_lender, stablecoin.balanceOf(flash_lender) + 10**21) - assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan - assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan + assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 - controller_factory.set_debt_ceiling(flash_lender, max_flash_loan * 2) + controller_factory.set_debt_ceiling(flash_lender, max_flash_loan * 2, sender=admin) - assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan * 2 - assert stablecoin.balanceOf(flash_lender) == max_flash_loan * 2 + 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan * 2 + assert stablecoin.balanceOf(flash_lender) == max_flash_loan * 2 + 10**21 -def test_decrease_debt_ceiling_with_excess(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): - with boa.env.prank(admin): - stablecoin.transfer(flash_lender, 10**21) +def test_decrease_debt_ceiling_with_excess( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): + boa.deal(stablecoin, flash_lender, stablecoin.balanceOf(flash_lender) + 10**21) - assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan - assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan + assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 - controller_factory.set_debt_ceiling(flash_lender, max_flash_loan // 2, sender=admin) + controller_factory.set_debt_ceiling(flash_lender, max_flash_loan // 2, sender=admin) - assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan // 2 - assert stablecoin.balanceOf(flash_lender) == max_flash_loan // 2 + 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan // 2 + assert stablecoin.balanceOf(flash_lender) == max_flash_loan // 2 + 10**21 -def test_empty_flash_lender_with_excess(controller_factory, flash_lender, stablecoin, max_flash_loan, admin): - with boa.env.prank(admin): - stablecoin.transfer(flash_lender, 10**21) +def test_empty_flash_lender_with_excess( + controller_factory, flash_lender, stablecoin, max_flash_loan, admin +): + boa.deal(stablecoin, flash_lender, stablecoin.balanceOf(flash_lender) + 10**21) - assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan - assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == max_flash_loan + assert stablecoin.balanceOf(flash_lender) == max_flash_loan + 10**21 - controller_factory.set_debt_ceiling(flash_lender, 0, sender=admin) + controller_factory.set_debt_ceiling(flash_lender, 0, sender=admin) - assert controller_factory.debt_ceiling_residual(flash_lender) == 0 - assert stablecoin.balanceOf(flash_lender) == 10**21 + assert controller_factory.debt_ceiling_residual(flash_lender) == 0 + assert stablecoin.balanceOf(flash_lender) == 10**21 diff --git a/tests/flashloan/test_flashloan.py b/tests/flashloan/test_flashloan.py index 80af6c72..4e34d2e9 100644 --- a/tests/flashloan/test_flashloan.py +++ b/tests/flashloan/test_flashloan.py @@ -7,7 +7,7 @@ def test_params(stablecoin, collateral_token, flash_lender, admin, max_flash_loa assert flash_lender.fee() == 0 assert flash_lender.flashFee(stablecoin, 10**18) == 0 - with boa.reverts('FlashLender: Unsupported currency'): + with boa.reverts("FlashLender: Unsupported currency"): flash_lender.flashFee(collateral_token, 10**18) assert flash_lender.maxFlashLoan(stablecoin) == stablecoin.balanceOf(flash_lender) @@ -16,7 +16,8 @@ def test_params(stablecoin, collateral_token, flash_lender, admin, max_flash_loa assert stablecoin.balanceOf(flash_lender) == max_flash_loan -def test_flashloan(stablecoin, flash_lender, flash_borrower, user, max_flash_loan): +def test_flashloan(stablecoin, flash_lender, flash_borrower, max_flash_loan): + user = boa.env.generate_address("user") for i in range(10): initial_count = flash_borrower.count() initial_total_amount = flash_borrower.total_amount() @@ -35,16 +36,19 @@ def test_flashloan(stablecoin, flash_lender, flash_borrower, user, max_flash_loa assert stablecoin.balanceOf(flash_lender) == max_flash_loan -def test_unsupported_currency(collateral_token, flash_borrower, user, max_flash_loan): +def test_unsupported_currency(collateral_token, flash_borrower, max_flash_loan): + user = boa.env.generate_address("user") with boa.reverts("FlashLender: Unsupported currency"): flash_borrower.flashBorrow(collateral_token, max_flash_loan, sender=user) -def test_too_much_to_lend(stablecoin, flash_borrower, user, max_flash_loan): +def test_too_much_to_lend(stablecoin, flash_borrower, max_flash_loan): + user = boa.env.generate_address("user") with boa.reverts(): flash_borrower.flashBorrow(stablecoin, max_flash_loan + 1, sender=user) -def test_repay_failed(stablecoin, flash_borrower, user, max_flash_loan): +def test_repay_failed(stablecoin, flash_borrower, max_flash_loan): + user = boa.env.generate_address("user") with boa.reverts("FlashLender: Repay failed"): flash_borrower.flashBorrow(stablecoin, max_flash_loan, False, sender=user) diff --git a/tests/boosted_lm_callback/__init__.py b/tests/forked/__init__.py similarity index 100% rename from tests/boosted_lm_callback/__init__.py rename to tests/forked/__init__.py diff --git a/tests/swap/__init__.py b/tests/forked/price_oracles/__init__.py similarity index 100% rename from tests/swap/__init__.py rename to tests/forked/price_oracles/__init__.py diff --git a/tests/forked/price_oracles/conftest.py b/tests/forked/price_oracles/conftest.py new file mode 100644 index 00000000..5f087110 --- /dev/null +++ b/tests/forked/price_oracles/conftest.py @@ -0,0 +1,74 @@ +import boa +import pytest +from .settings import WEB3_PROVIDER_URL, EXPLORER_URL, EXPLORER_TOKEN + + +@pytest.fixture(scope="module", autouse=True) +def boa_fork(): + assert WEB3_PROVIDER_URL is not None, ( + "Provider url is not set, add WEB3_PROVIDER_URL param to env" + ) + boa.fork(WEB3_PROVIDER_URL) + + +@pytest.fixture(scope="module") +def stablecoin_aggregator(): + return boa.from_etherscan( + "0x18672b1b0c623a30089A280Ed9256379fb0E4E62", + "AggregatorStablePrice", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) # USD/crvUSD + + +@pytest.fixture(scope="module") +def admin(): + return boa.env.generate_address() + + +@pytest.fixture(scope="module") +def trader(): + return boa.env.generate_address() + + +@pytest.fixture(scope="module") +def stable_impl(admin): + with boa.env.prank(admin): + return boa.load_partial( + "contracts/price_oracles/lp-oracles/LPOracleStable.vy" + ).deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def crypto_impl(admin): + with boa.env.prank(admin): + return boa.load_partial( + "contracts/price_oracles/lp-oracles/LPOracleCrypto.vy" + ).deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def proxy_impl(admin): + with boa.env.prank(admin): + return boa.load("contracts/price_oracles/proxy/ProxyOracle.vy") + + +@pytest.fixture(scope="module") +def proxy_factory(admin, proxy_impl): + with boa.env.prank(admin): + return boa.load( + "contracts/price_oracles/proxy/ProxyOracleFactory.vy", admin, proxy_impl + ) + + +@pytest.fixture(scope="module") +def lp_oracle_factory(admin, stable_impl, crypto_impl, proxy_factory): + with boa.env.prank(admin): + factory = boa.load( + "contracts/price_oracles/lp-oracles/LPOracleFactory.vy", + admin, + stable_impl, + crypto_impl, + proxy_factory, + ) + return factory diff --git a/tests_forked/price_oracles/settings.py b/tests/forked/price_oracles/settings.py similarity index 100% rename from tests_forked/price_oracles/settings.py rename to tests/forked/price_oracles/settings.py diff --git a/tests/forked/price_oracles/test_lp_oracle_compare_to_spot.py b/tests/forked/price_oracles/test_lp_oracle_compare_to_spot.py new file mode 100644 index 00000000..affb2210 --- /dev/null +++ b/tests/forked/price_oracles/test_lp_oracle_compare_to_spot.py @@ -0,0 +1,769 @@ +import boa +from .settings import EXPLORER_URL, EXPLORER_TOKEN + + +def test_tricrypto_usdc(lp_oracle_factory, stablecoin_aggregator, admin, trader): + tricrypto_usdc_pool_address = "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" + crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" + + tricrypto_usdc_pool = boa.from_etherscan( + tricrypto_usdc_pool_address, + "TricryptoUSDC", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + crvusd_usdc_pool = boa.from_etherscan( + crvusd_usdc_pool_address, + "crvUSD/USDC", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + + usdc_crvusd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRate.vy", + [crvusd_usdc_pool_address], + [1], + [0], + ) # crvUSD/USDC + usdc_usd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRateWAgg.vy", + [crvusd_usdc_pool_address], + [1], + [0], + stablecoin_aggregator.address, + ) # USD/USDC + with boa.env.prank(admin): + tricrypto_usdc_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrypto_usdc_pool_address, usdc_crvusd_oracle.address + )[1] + ) # USDC/LP * crvUSD/USDC + tricrypto_usdc_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrypto_usdc_pool_address, usdc_usd_oracle.address + )[1] + ) # USDC/LP * crvUSD/USDC * USD/crvUSD + + # --- Compare oracle and spot prices --- + + erc_mock = boa.load_partial("contracts/testing/ERC20Mock.vy") + usdc = erc_mock.at(tricrypto_usdc_pool.coins(0)) + wbtc = erc_mock.at(tricrypto_usdc_pool.coins(1)) + boa.deal(usdc, trader, usdc.balanceOf(tricrypto_usdc_pool) * 10) + boa.deal(wbtc, trader, wbtc.balanceOf(tricrypto_usdc_pool) * 10) + initial_wbtc_spot_price = tricrypto_usdc_pool.get_dy(1, 0, 10**4) + + with boa.env.prank(trader): + usdc.approve(tricrypto_usdc_pool, 2**256 - 1) + wbtc.approve(tricrypto_usdc_pool, 2**256 - 1) + + prices_to_check = [ + int(p * initial_wbtc_spot_price) + for p in [ + 0.40, + 0.50, + 0.60, + 0.70, + 0.80, + 0.90, + 0.92, + 0.95, + 0.98, + 1.00, + 1.02, + 1.05, + 1.08, + 1.10, + 1.20, + 1.30, + 1.40, + 1.50, + 1.60, + 1.80, + 2.00, + ] + ] + for target_p in prices_to_check: + while tricrypto_usdc_pool.get_dy(1, 0, 10**4) > target_p: + tricrypto_usdc_pool.exchange( + 1, 0, wbtc.balanceOf(tricrypto_usdc_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + while tricrypto_usdc_pool.get_dy(1, 0, 10**4) < target_p: + tricrypto_usdc_pool.exchange( + 0, 1, usdc.balanceOf(tricrypto_usdc_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = tricrypto_usdc_crvusd_lp_oracle.price() + lp_oracle_price_usd = tricrypto_usdc_usd_lp_oracle.price() + + # Spot price + usdc_from_lp = tricrypto_usdc_pool.calc_withdraw_one_coin(10**18, 0) + crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) + lp_spot_price_crvusd = crvusd_from_lp + lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 + + delta = 1e-3 + print( + "p =", + target_p / 10**2, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) + + +def test_tricrypto_usdt(lp_oracle_factory, stablecoin_aggregator, admin, trader): + tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" + crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" + + tricrypto_usdt_pool = boa.from_etherscan( + tricrypto_usdt_pool_address, + "TricryptoUSDT", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + crvusd_usdt_pool = boa.from_etherscan( + crvusd_usdt_pool_address, + "crvUSD/USDT", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + + usdt_crvusd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRate.vy", + [crvusd_usdt_pool_address], + [1], + [0], + ) # crvUSD/USDT + usdt_usd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRateWAgg.vy", + [crvusd_usdt_pool_address], + [1], + [0], + stablecoin_aggregator.address, + ) # USD/USDT + with boa.env.prank(admin): + tricrypto_usdt_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrypto_usdt_pool_address, usdt_crvusd_oracle.address + )[1] + ) # USDT/LP * crvUSD/USDT + tricrypto_usdt_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrypto_usdt_pool_address, usdt_usd_oracle.address + )[1] + ) # USDT/LP * crvUSD/USDT * USD/crvUSD + + # --- Compare oracle and spot prices --- + + # USDT interface is different (does not return bool from 'approve' method, for example) + usdt = boa.from_etherscan( + tricrypto_usdt_pool.coins(0), "USDT", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) + wbtc = boa.load_partial("contracts/testing/ERC20Mock.vy").at( + tricrypto_usdt_pool.coins(1) + ) + boa.deal(usdt, trader, usdt.balanceOf(tricrypto_usdt_pool) * 10) + boa.deal(wbtc, trader, wbtc.balanceOf(tricrypto_usdt_pool) * 10) + initial_wbtc_spot_price = tricrypto_usdt_pool.get_dy(1, 0, 10**4) + + with boa.env.prank(trader): + usdt.approve(tricrypto_usdt_pool, 2**256 - 1) + wbtc.approve(tricrypto_usdt_pool, 2**256 - 1) + + prices_to_check = [ + int(p * initial_wbtc_spot_price) + for p in [ + 0.40, + 0.50, + 0.60, + 0.70, + 0.80, + 0.90, + 0.92, + 0.95, + 0.98, + 1.00, + 1.02, + 1.05, + 1.08, + 1.10, + 1.20, + 1.30, + 1.40, + 1.50, + 1.60, + 1.80, + 2.00, + ] + ] + for target_p in prices_to_check: + while tricrypto_usdt_pool.get_dy(1, 0, 10**4) > target_p: + tricrypto_usdt_pool.exchange( + 1, 0, wbtc.balanceOf(tricrypto_usdt_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + while tricrypto_usdt_pool.get_dy(1, 0, 10**4) < target_p: + tricrypto_usdt_pool.exchange( + 0, 1, usdt.balanceOf(tricrypto_usdt_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = tricrypto_usdt_crvusd_lp_oracle.price() + lp_oracle_price_usd = tricrypto_usdt_usd_lp_oracle.price() + + # Spot price + usdt_from_lp = tricrypto_usdt_pool.calc_withdraw_one_coin(10**18, 0) + crvusd_from_lp = crvusd_usdt_pool.get_dy(0, 1, usdt_from_lp) + lp_spot_price_crvusd = crvusd_from_lp + lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 + + delta = 6e-3 + print( + "p =", + target_p / 10**2, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) + + +def test_tricrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): + tricrv_pool_address = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" + + tricrv_pool = boa.from_etherscan( + tricrv_pool_address, "TriCRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) + with boa.env.prank(admin): + tricrv_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrv_pool_address, "0x0000000000000000000000000000000000000000" + )[1] + ) # USDT/LP * crvUSD/USDT + tricrv_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + tricrv_pool_address, stablecoin_aggregator.address + )[1] + ) # USDT/LP * crvUSD/USDT * USD/crvUSD + + # --- Compare oracle and spot prices --- + + crvusd = boa.load_partial("contracts/testing/ERC20Mock.vy").at(tricrv_pool.coins(0)) + weth = boa.load_partial("contracts/testing/WETH.vy").at(tricrv_pool.coins(1)) + boa.deal(crvusd, trader, crvusd.balanceOf(tricrv_pool) * 10) + boa.env.set_balance(trader, boa.env.get_balance(tricrv_pool.address) * 10) + weth.deposit(sender=trader, value=boa.env.get_balance(tricrv_pool.address) * 10) + initial_weth_spot_price = tricrv_pool.get_dy(1, 0, 10**12) + + with boa.env.prank(trader): + crvusd.approve(tricrv_pool, 2**256 - 1) + weth.approve(tricrv_pool, 2**256 - 1) + + prices_to_check = [ + int(p * initial_weth_spot_price) + for p in [ + 0.40, + 0.50, + 0.60, + 0.70, + 0.80, + 0.90, + 0.92, + 0.95, + 0.98, + 1.00, + 1.02, + 1.05, + 1.08, + 1.10, + 1.20, + 1.30, + 1.40, + 1.50, + 1.60, + 1.80, + 2.00, + ] + ] + for target_p in prices_to_check: + while tricrv_pool.get_dy(1, 0, 10**12) > target_p: + tricrv_pool.exchange( + 1, 0, boa.env.get_balance(tricrv_pool.address) // 500, 0 + ) + boa.env.time_travel(3600) + + while tricrv_pool.get_dy(1, 0, 10**12) < target_p: + tricrv_pool.exchange(0, 1, crvusd.balanceOf(tricrv_pool) // 500, 0) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = tricrv_crvusd_lp_oracle.price_w() + lp_oracle_price_usd = tricrv_usd_lp_oracle.price_w() + + # Spot price + crvusd_from_lp = tricrv_pool.calc_withdraw_one_coin(10**18, 0) + lp_spot_price_crvusd = crvusd_from_lp + lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 + + delta = 2e-3 if target_p / initial_weth_spot_price < 0.5 else 1e-3 + print( + "p =", + target_p / 10**12, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) + + +def test_strategic_reserve(lp_oracle_factory, stablecoin_aggregator, admin, trader): + strategic_reserve_pool_address = "0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85" + crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" + + strategic_reserve_pool = boa.from_etherscan( + strategic_reserve_pool_address, + "StrategicReserveUSD", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + crvusd_usdc_pool = boa.from_etherscan( + crvusd_usdc_pool_address, + "crvUSD/USDC", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + + usdc_crvusd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRate.vy", + [crvusd_usdc_pool_address], + [1], + [0], + ) # crvUSD/USDC + usdc_usd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRateWAgg.vy", + [crvusd_usdc_pool_address], + [1], + [0], + stablecoin_aggregator.address, + ) # USD/USDC + with boa.env.prank(admin): + strategic_reserve_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + strategic_reserve_pool_address, usdc_crvusd_oracle.address + )[1] + ) # USDC/LP * crvUSD/USDC + strategic_reserve_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + strategic_reserve_pool_address, usdc_usd_oracle.address + )[1] + ) # USDC/LP * crvUSD/USDC * USD/crvUSD + + # --- Compare oracle and spot prices --- + + erc_mock = boa.load_partial("contracts/testing/ERC20Mock.vy") + usdc = erc_mock.at(strategic_reserve_pool.coins(0)) + # USDT interface is different (does not return bool from 'approve' method, for example) + usdt = boa.from_etherscan( + strategic_reserve_pool.coins(1), + "USDT", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + boa.deal(usdc, trader, usdc.balanceOf(strategic_reserve_pool) * 10) + boa.deal(usdt, trader, usdt.balanceOf(strategic_reserve_pool) * 10) + with boa.env.prank(trader): + usdc.approve(strategic_reserve_pool, 2**256 - 1) + usdt.approve(strategic_reserve_pool, 2**256 - 1) + + prices_to_check = [ + p * 10**16 + for p in [ + 40, + 50, + 60, + 70, + 80, + 90, + 92, + 95, + 98, + 100, + 102, + 105, + 108, + 110, + 120, + 130, + 140, + 150, + 160, + 180, + 200, + ] + ] + for target_p in prices_to_check: + while strategic_reserve_pool.get_p(0) > target_p: + strategic_reserve_pool.exchange( + 1, 0, usdt.balanceOf(strategic_reserve_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + while strategic_reserve_pool.get_p(0) < target_p: + strategic_reserve_pool.exchange( + 0, 1, usdc.balanceOf(strategic_reserve_pool) // 500, 0 + ) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = strategic_reserve_crvusd_lp_oracle.price() + lp_oracle_price_usd = strategic_reserve_usd_lp_oracle.price() + + # Spot price + usdc_from_lp = strategic_reserve_pool.calc_withdraw_one_coin(10**18, 0) + crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) + lp_spot_price_crvusd = crvusd_from_lp + lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 + + delta = 0.0011 * (1 + 0.15 * (abs(target_p - 10**18) / 10**16)) + print( + "p =", + target_p / 10**18, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) + + +def test_weeth_weth(lp_oracle_factory, stablecoin_aggregator, admin, trader): + weeth_ng_pool_address = "0xDB74dfDD3BB46bE8Ce6C33dC9D82777BCFc3dEd5" + tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" + crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" + + weeth_ng_pool = boa.from_etherscan( + weeth_ng_pool_address, "weETH-ng", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) + tricrypto_usdt_pool = boa.from_etherscan( + tricrypto_usdt_pool_address, + "TricryptoUSDT", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + crvusd_usdt_pool = boa.from_etherscan( + crvusd_usdt_pool_address, + "crvUSD/USDT", + uri=EXPLORER_URL, + api_key=EXPLORER_TOKEN, + ) + + usdt_crvusd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRate.vy", + [tricrypto_usdt_pool_address, crvusd_usdt_pool_address], + [0, 1], + [2, 0], + ) # crvUSD/ETH + usdt_usd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRateWAgg.vy", + [tricrypto_usdt_pool_address, crvusd_usdt_pool_address], + [0, 1], + [2, 0], + stablecoin_aggregator.address, + ) # USD/ETH + with boa.env.prank(admin): + weeth_ng_pool_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + weeth_ng_pool_address, usdt_crvusd_oracle.address + )[1] + ) # ETH/LP * crvUSD/ETH + weeth_ng_pool_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle( + weeth_ng_pool_address, usdt_usd_oracle.address + )[1] + ) # ETH/LP * crvUSD/ETH * USD/crvUSD + + # --- Compare oracle and spot prices --- + + weth = boa.load_partial("contracts/testing/WETH.vy").at(weeth_ng_pool.coins(0)) + weeth = boa.load_partial("contracts/testing/ERC20Mock.vy").at( + weeth_ng_pool.coins(1) + ) + boa.env.set_balance(trader, weth.balanceOf(weeth_ng_pool) * 10) + weth.deposit(sender=trader, value=weth.balanceOf(weeth_ng_pool) * 10) + boa.deal(weeth, trader, weeth.balanceOf(weeth_ng_pool) * 10) + + with boa.env.prank(trader): + weth.approve(weeth_ng_pool, 2**256 - 1) + weeth.approve(weeth_ng_pool, 2**256 - 1) + + # Align TricryptoUSDT oracle + weth.approve(tricrypto_usdt_pool, 2**256 - 1) + for i in range(100): + tricrypto_usdt_pool.exchange(2, 0, 10**9, 0) + boa.env.time_travel(3600) + + prices_to_check = [ + p * 10**16 + for p in [ + 40, + 50, + 60, + 70, + 80, + 90, + 92, + 95, + 98, + 100, + 102, + 105, + 108, + 110, + 120, + 130, + 140, + 150, + 160, + 180, + 200, + ] + ] + for target_p in prices_to_check: + while weeth_ng_pool.get_p(0) > target_p: + weeth_ng_pool.exchange(1, 0, weeth.balanceOf(weeth_ng_pool) // 500, 0) + boa.env.time_travel(3600) + + while weeth_ng_pool.get_p(0) < target_p: + weeth_ng_pool.exchange(0, 1, weth.balanceOf(weeth_ng_pool) // 500, 0) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = weeth_ng_pool_crvusd_lp_oracle.price() + lp_oracle_price_usd = weeth_ng_pool_usd_lp_oracle.price() + + # Spot price + eth_from_lp = weeth_ng_pool.calc_withdraw_one_coin(10**15, 0) + usdt_from_lp = tricrypto_usdt_pool.get_dy(2, 0, eth_from_lp) + crvusd_from_lp = crvusd_usdt_pool.get_dy(0, 1, usdt_from_lp) + lp_spot_price_crvusd = crvusd_from_lp * 1000 + lp_spot_price_usd = ( + lp_spot_price_crvusd * stablecoin_aggregator.price() // 10**18 + ) + + delta = 0.005 if 70 * 10**16 < target_p < 130 * 10**16 else 0.015 + print( + "p =", + target_p / 10**18, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) + + +def test_cvxcrv(lp_oracle_factory, stablecoin_aggregator, admin, trader): + cvxcrv_pool_address = "0x971add32Ea87f10bD192671630be3BE8A11b8623" + tricrv_pool_address = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" + + cvxcrv_pool = boa.from_etherscan( + cvxcrv_pool_address, "cvxCRV/CRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) + tricrv_pool = boa.from_etherscan( + tricrv_pool_address, "TriCRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) + + crv_crvusd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRate.vy", + [tricrv_pool_address], + [0], + [2], + ) # crvUSD/CRV + crv_usd_oracle = boa.load( + "contracts/price_oracles/CryptoFromPoolsRateWAgg.vy", + [tricrv_pool_address], + [0], + [2], + stablecoin_aggregator.address, + ) # USD/CRV + with boa.env.prank(admin): + cvxcrv_pool_crvusd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_crvusd_oracle)[1] + ) # CRV/LP * crvUSD/CRV + cvxcrv_pool_usd_lp_oracle = boa.load_partial( + "contracts/price_oracles/proxy/ProxyOracle.vy" + ).at( + lp_oracle_factory.deploy_oracle(cvxcrv_pool_address, crv_usd_oracle)[1] + ) # CRV/LP * crvUSD/CRV * USD/crvUSD + + # --- Compare oracle and spot prices --- + + erc_mock = boa.load_partial("contracts/testing/ERC20Mock.vy") + crv = erc_mock.at(cvxcrv_pool.coins(0)) + cvxcrv = erc_mock.at(cvxcrv_pool.coins(1)) + boa.deal(crv, trader, crv.balanceOf(cvxcrv_pool) * 20) + boa.deal(cvxcrv, trader, cvxcrv.balanceOf(cvxcrv_pool) * 20) + with boa.env.prank(trader): + crv.approve(cvxcrv_pool, 2**256 - 1) + cvxcrv.approve(cvxcrv_pool, 2**256 - 1) + + prices_to_check = [ + p * 10**16 + for p in [ + 40, + 50, + 60, + 70, + 80, + 90, + 92, + 95, + 98, + 100, + 102, + 105, + 108, + 110, + 120, + 130, + 140, + 150, + 160, + 180, + 200, + ] + ] + for target_p in prices_to_check: + while cvxcrv_pool.get_p() > target_p: + cvxcrv_pool.exchange(1, 0, cvxcrv.balanceOf(cvxcrv_pool) // 500, 0) + boa.env.time_travel(3600) + + while cvxcrv_pool.get_p() < target_p: + cvxcrv_pool.exchange(0, 1, crv.balanceOf(cvxcrv_pool) // 500, 0) + boa.env.time_travel(3600) + + # Oracle price + lp_oracle_price_crvusd = cvxcrv_pool_crvusd_lp_oracle.price_w() + lp_oracle_price_usd = cvxcrv_pool_usd_lp_oracle.price_w() + + # Spot price + crv_from_lp = cvxcrv_pool.calc_withdraw_one_coin(10**18, 0) + crvusd_from_lp = tricrv_pool.get_dy(2, 0, crv_from_lp) + lp_spot_price_crvusd = crvusd_from_lp + lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 + + delta = 0.002 * (1 + 1.3 * (abs(target_p - 10**18) / 10**16)) + print( + "p =", + target_p / 10**18, + "delta =", + abs(lp_spot_price_usd - lp_oracle_price_usd) + / lp_oracle_price_usd + * 100, + "%", + "<", + delta * 100, + "%", + ) + assert ( + abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) + / lp_oracle_price_crvusd + < delta + ) + assert ( + abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd + < delta + ) diff --git a/tests_brownie/__init__.py b/tests/fuzz/__init__.py similarity index 100% rename from tests_brownie/__init__.py rename to tests/fuzz/__init__.py diff --git a/tests_brownie/stablecoin/__init__.py b/tests/fuzz/stateful/__init__.py similarity index 100% rename from tests_brownie/stablecoin/__init__.py rename to tests/fuzz/stateful/__init__.py diff --git a/tests/fuzz/stateful/test_controller_stateful.py b/tests/fuzz/stateful/test_controller_stateful.py new file mode 100644 index 00000000..42ba5d55 --- /dev/null +++ b/tests/fuzz/stateful/test_controller_stateful.py @@ -0,0 +1,306 @@ +from decimal import Decimal +from hypothesis import event, note, assume +from hypothesis.strategies import ( + composite, + integers, + decimals, + SearchStrategy, + data, + sampled_from, +) +from hypothesis.stateful import ( + RuleBasedStateMachine, + initialize, + rule, + precondition, + invariant, +) + +import boa + +from tests.fuzz.strategies import mint_markets, ticks +from tests.utils.deployers import AMM_DEPLOYER, ERC20_MOCK_DEPLOYER, STABLECOIN_DEPLOYER +from tests.utils.constants import ( + MAX_UINT256, + WAD, +) + + +def token_amounts( + decimal_places: int, min_value: int = 0, max_value: int = None +) -> SearchStrategy[int]: + decs = decimals( + min_value=min_value, + max_value=max_value, + allow_nan=False, + allow_infinity=False, + places=decimal_places, + ) + + def strip_point_to_int(d: Decimal) -> int: + t = d.as_tuple() # (sign, digits, exponent) + # Join all significant digits; if exponent > 0, append that many zeros. + # If exponent <= 0, we just drop the point (no padding). + digits = "".join(map(str, t.digits)) or "0" + zeros = "0" * max(t.exponent, 0) + return int(digits + zeros) + + return decs.map(strip_point_to_int) + + +@composite +def loan_amounts_for_create(draw, controller, N: int) -> tuple[int, int]: + """ + Draw a (collateral, debt) pair valid for Controller.create_loan given N. + + - collateral is drawn using `token_amounts` based on the collateral token's decimals. + - debt is drawn in [1, max_borrowable(collateral, N)] so `_calculate_debt_n1` won't revert. + """ + collateral_token = ERC20_MOCK_DEPLOYER.at(controller.collateral_token()) + token_decs = collateral_token.decimals() + + # Draw collateral with bounded range to reduce extreme ratios + collateral = draw(token_amounts(token_decs, min_value=1, max_value=1_000_000)) + + # Compute an upper bound for debt using both the on-chain view and LTV + view_cap = controller.max_borrowable(collateral, N) + + # LTV-based cap: debt <= collateral_value * (1 - loan_discount) + loan_discount = controller.loan_discount() + ltv = max(WAD - loan_discount, 0) + + # Precision scale factors from token decimals + borrowed_token = ERC20_MOCK_DEPLOYER.at(controller.borrowed_token()) + borrowed_decs = borrowed_token.decimals() + col_prec = 10 ** (18 - int(token_decs)) + bor_prec = 10 ** (18 - int(borrowed_decs)) + + # Price at 1e18 from AMM + price = AMM_DEPLOYER.at(controller.amm()).price_oracle() + + collateral_value_18 = (collateral * col_prec * price) // WAD + ltv_cap_18 = (collateral_value_18 * ltv) // WAD + ltv_cap = ltv_cap_18 // bor_prec + + max_debt = min(view_cap, ltv_cap) + + # Skip zero-cap cases + assume(max_debt > 0) + + # Draw debt within [max(view_cap/32, 1), view_cap] to avoid tiny-debt cases + # that push n1 beyond exponent safety bounds in AMM.p_oracle_up + min_debt = max(1, max_debt // 32) + debt = draw(integers(min_value=min_debt, max_value=max_debt)) + return collateral, debt + + +@composite +def loan_increments_for_borrow_more( + draw, + controller, + user: str, + N: int, + collateral0: int, + debt0: int, +) -> tuple[int, int]: + """ + Draw (d_collateral, d_debt) increments for a safe borrow_more call. + + - d_collateral: decimals-aware collateral increment with bounded range. + - d_debt: drawn within a safe fraction of the available headroom derived from + controller.max_borrowable(collateral0 + d_collateral, N, debt0, user). + """ + collateral_token = ERC20_MOCK_DEPLOYER.at(controller.collateral_token()) + token_decs = collateral_token.decimals() + + # Reasonable collateral increment to avoid extreme ratios + d_collateral = draw(token_amounts(token_decs, min_value=1, max_value=1_000_000)) + + # Compute available headroom and draw delta debt safely within it + cap_total = controller.max_borrowable(collateral0 + d_collateral, N, debt0, user) + assume(cap_total > debt0) + delta_cap = cap_total - debt0 + min_delta = max(1, delta_cap // 32) + d_debt = draw(integers(min_value=min_delta, max_value=delta_cap)) + + return d_collateral, d_debt + + +# ---------------- mint market via Protocol ---------------- + + +# ------------ controller interaction params ------------- + + +class ControllerStateful(RuleBasedStateMachine): + @initialize(market=mint_markets()) + def _initialize(self, market): + # Unpack market artifacts + self.controller = market["controller"] + self.amm = market["amm"] + self.users = [] + self.collateral_token = ERC20_MOCK_DEPLOYER.at( + self.controller.collateral_token() + ) + self.borrowed_token = STABLECOIN_DEPLOYER.at(self.controller.borrowed_token()) + self.borrowed_token.approve(self.controller.address, MAX_UINT256) + + @rule(N=ticks, data=data()) + def create_loan_rule(self, N, data): + note("[CREATE LOAN]") + # New user per invocation + user_label = f"user_{len(self.users)}" + user = boa.env.generate_address(user_label) + + # Draw a valid (collateral, debt) pair for this controller and N + collateral, debt = data.draw( + loan_amounts_for_create(self.controller, N), + label=f"loan_amounts_for_create({user_label})", + ) + + # Deal collateral on site and approve controller + note( + f"creating loan: N={N}, user={user_label}, collateral={collateral}, debt={debt}, " + f"collat_decs={self.collateral_token.decimals()}, loan_discount={self.controller.loan_discount()}" + ) + boa.deal(self.collateral_token, user, collateral) + with boa.env.prank(user): + self.collateral_token.approve(self.controller.address, MAX_UINT256) + self.controller.create_loan(collateral, debt, N) + + self.users.append(user) + + @invariant() + def time_passes(self): + # Snapshot current debts for tracked users + before = {u: self.controller.debt(u) for u in self.users} + + # Advance time and update AMM rate based on monetary policy + dt = 3600 # one hour + boa.env.time_travel(dt) + self.controller.save_rate() + + # Debts should not decrease with time + for u, d0 in before.items(): + d1 = self.controller.debt(u) + assert d1 >= d0 + + @precondition(lambda self: len(self.users) > 0) + @rule(data=data()) + def repay_rule(self, data): + note("[REPAY]") + # Pick a random tracked user (open position invariant ensures this is valid) + user = data.draw(sampled_from(self.users), label="repay_user") + + # Current debt and repay amount in [1, debt] + debt = self.controller.debt(user) + assume(debt > 0) + repay_amount = data.draw( + integers(min_value=1, max_value=debt), label="repay_amount" + ) + + # Ensure user has enough borrowed tokens and allowance + borrowed = STABLECOIN_DEPLOYER.at(self.controller.borrowed_token()) + boa.deal(borrowed, user, repay_amount) + with boa.env.prank(user): + borrowed.approve(self.controller.address, MAX_UINT256) + self.controller.repay(repay_amount) + + # If fully repaid, remove from tracking list + if not self.controller.loan_exists(user): + self.users.remove(user) + + note(f"repay: user={user}, debt_before={debt}, repay_amount={repay_amount}") + + @precondition(lambda self: len(self.users) > 0) + @rule(data=data()) + def borrow_more_rule(self, data): + note("[BORROW MORE]") + # Pick a random tracked user and ensure position is healthy + user = data.draw(sampled_from(self.users), label="borrow_more_user") + assume(self.controller.health(user, False) > 0) + + # Fetch current state + collateral0, _x0, debt0, N = self.controller.user_state(user) + + # Draw increments using the shared strategy + d_collateral, d_debt = data.draw( + loan_increments_for_borrow_more( + self.controller, user, N, collateral0, debt0 + ), + label="borrow_more_increments", + ) + + # Fund collateral and call borrow_more as the user + boa.deal(self.collateral_token, user, d_collateral) + with boa.env.prank(user): + # Approve is already set at creation, but re-approve is harmless + self.collateral_token.approve(self.controller.address, MAX_UINT256) + self.controller.borrow_more(d_collateral, d_debt) + + @precondition(lambda self: len(self.users) > 0) + @rule(data=data()) + def remove_collateral_rule(self, data): + note("[REMOVE COLLATERAL]") + + # Pick a random tracked user and ensure position is healthy + user = data.draw(sampled_from(self.users), label="remove_collateral_user") + + # Read current state + collateral, _x, debt, N = self.controller.user_state(user) + + # Compute the maximum removable collateral using min_collateral + min_coll = self.controller.min_collateral(debt, N, user) + assert collateral >= min_coll + removable = collateral - min_coll + + if removable == 0: + event(f"user {user} has no removable collateral") + return + + # Draw a safe removal amount, avoid edge rounding by not always taking full + min_remove = max(1, removable // 64) + d_collateral = data.draw( + integers(min_value=min_remove, max_value=removable), + label="remove_collateral_amount", + ) + + # Call as user + with boa.env.prank(user): + self.controller.remove_collateral(d_collateral) + + @invariant() + def open_users_invariant(self): + # On-chain enumeration of open loans + n = self.controller.n_loans() + onchain_users = [self.controller.loans(i) for i in range(n)] + # Ensure tracking matches on-chain state + assert set(onchain_users) == set(self.users) + assert len(onchain_users) == len(self.users) + for u in self.users: + assert self.controller.loan_exists(u) + + @precondition(lambda self: len(self.users) > 0) + @invariant() + def liquidate(self): + positions = self.controller.users_to_liquidate(0, len(self.users)) + if len(positions) == 0: + return + + note("[HARD LIQUIDATE]") + for pos in positions: + note( + f"liquidating user {pos.user} with health {self.controller.health(pos.user, True)}" + ) + required = self.controller.tokens_to_liquidate(pos.user, WAD) + if required > 0: + boa.deal(self.borrowed_token, boa.env.eoa, required) + self.controller.liquidate(pos.user, 0, WAD) + + assert len(self.controller.users_to_liquidate(0, len(self.users))) == 0 + + n = self.controller.n_loans() + self.users = [self.controller.loans(i) for i in range(n)] + + +TestControllerStateful = ControllerStateful.TestCase diff --git a/tests/fuzz/stateful/test_lend_controller_stateful.py b/tests/fuzz/stateful/test_lend_controller_stateful.py new file mode 100644 index 00000000..68e3d1a2 --- /dev/null +++ b/tests/fuzz/stateful/test_lend_controller_stateful.py @@ -0,0 +1,84 @@ +import boa +from tests.fuzz.stateful.test_controller_stateful import ControllerStateful +from tests.fuzz.strategies import lend_markets + +from hypothesis import note, assume +from hypothesis.strategies import data, integers, sampled_from +from hypothesis.stateful import rule, initialize, precondition + +from tests.utils.deployers import ERC20_MOCK_DEPLOYER +from tests.utils.constants import MIN_ASSETS +from tests.utils import max_approve + + +class LendControllerStateful(ControllerStateful): + @initialize(market=lend_markets()) + def _initialize(self, market): + super()._initialize(market) + self.vault = market["vault"] + self.vault_users = [] + + def users_allowed_to_withdraw(self): + return [user for user in self.vault_users if self.vault.maxWithdraw(user) > 0] + + @precondition(lambda self: self.vault.maxSupply() > self.vault.totalAssets()) + @rule(data=data()) + def deposit(self, data): + note("[DEPOSIT]") + user_label = f"vault_user_{len(self.vault_users)}" + user = boa.env.generate_address(user_label) + + vault = self.vault + max_deposit = int(vault.maxDeposit(user)) + assume(max_deposit > 0) + + current_assets = int(vault.totalAssets()) + min_required = ( + 1 if current_assets >= MIN_ASSETS else MIN_ASSETS - current_assets + ) + + borrowed_token = ERC20_MOCK_DEPLOYER.at(vault.borrowed_token()) # type: ignore[attr-defined] + token_decimals = int(borrowed_token.decimals()) # type: ignore[attr-defined] + upper_bound = min(max_deposit, 10**6 * 10**token_decimals) + assume(min_required <= upper_bound) + + amount = data.draw( + integers(min_value=min_required, max_value=upper_bound), + label=f"deposit_amount({user_label})", + ) + + note( + f"depositing: user={user_label}, amount={amount}, max_deposit={max_deposit}" + ) + + boa.deal(borrowed_token, user, amount) + with boa.env.prank(user): + max_approve(borrowed_token, vault.address) + vault.deposit(amount, user) + + self.vault_users.append(user) + + @precondition(lambda self: self.users_allowed_to_withdraw()) + @rule(data=data()) + def withdraw(self, data): + note("[WITHDRAW]") + vault = self.vault + user = data.draw( + sampled_from(self.users_allowed_to_withdraw()), label="withdraw_user" + ) + + user_shares = vault.balanceOf(user) + + amount = data.draw( + integers(min_value=1, max_value=min(user_shares, vault.maxWithdraw(user))), + label=f"withdraw_amount({user})", + ) + + note(f"withdrawing: user={user}, amount={amount}, shares={user_shares}") + + vault.withdraw(amount, user, sender=user) + if amount == user_shares: + self.vault_users.remove(user) + + +TestLendControllerStateful = LendControllerStateful.TestCase diff --git a/tests/fuzz/strategies.py b/tests/fuzz/strategies.py new file mode 100644 index 00000000..ca44e50f --- /dev/null +++ b/tests/fuzz/strategies.py @@ -0,0 +1,136 @@ +from hypothesis import note +from hypothesis.strategies import ( + composite, + integers, + builds, +) + +from tests.utils.constants import ( + MAX_UINT256, + MIN_A, + MAX_A, + MIN_FEE, + MAX_FEE, + MAX_LOAN_DISCOUNT, + MIN_LIQUIDATION_DISCOUNT, + MIN_TICKS, + MAX_TICKS, +) +from tests.utils.deployers import ERC20_MOCK_DEPLOYER +from tests.utils.protocols import Llamalend + +DEBT_CEILING_MAX = 10**8 * 10**18 # TODO this has to go + + +As = integers(min_value=MIN_A, max_value=MAX_A) +amm_fees = integers(min_value=MIN_FEE, max_value=MAX_FEE) +# Debt ceiling is a uint256 on-chain; generate as an integer +debt_ceilings = integers(min_value=0, max_value=DEBT_CEILING_MAX) +token_decimals = integers(min_value=2, max_value=18) +prices = integers(min_value=int(1e12), max_value=int(1e24)) + +# A simple strategy to initialize Llamalend using Hypothesis builds +protocols = builds(Llamalend, initial_price=prices) + +# A simple strategy to deploy a collateral token with fuzzed decimals +collaterals = builds(ERC20_MOCK_DEPLOYER.deploy, token_decimals) + + +@composite +def discounts(draw): + """Draw (loan_discount, liquidation_discount) with loan > liquidation.""" + liq = draw( + integers(min_value=MIN_LIQUIDATION_DISCOUNT, max_value=MAX_LOAN_DISCOUNT - 1) + ) + loan = draw( + integers( + min_value=(max(liq, MIN_LIQUIDATION_DISCOUNT) + 1), + max_value=MAX_LOAN_DISCOUNT, + ) + ) + return loan, liq + + +@composite +def mint_markets( + draw, + As=As, + amm_fees=amm_fees, + discounts=discounts(), + debt_ceilings=debt_ceilings, + initial_prices=prices, +): + _A = draw(As) + _fee = draw(amm_fees) + _loan_discount, _liq_discount = draw(discounts) + _dc = draw(debt_ceilings) + _price = draw(initial_prices) + + proto = Llamalend(initial_price=_price) + + _collateral = draw(collaterals) + _dec = _collateral.decimals() + + market = proto.create_mint_market( + collateral_token=_collateral, + price_oracle=proto.price_oracle, + monetary_policy=proto.mint_monetary_policy, + A=_A, + amm_fee=_fee, + loan_discount=_loan_discount, + liquidation_discount=_liq_discount, + debt_ceiling=MAX_UINT256, + ) + + note( + "deployed mint market with " + + f"A={_A}, fee={_fee}, loan_discount={_loan_discount}, liq_discount={_liq_discount}, debt_ceiling={_dc}" + + f"; decimals={_dec}, price={_price}" + ) + + return market + + +@composite +def lend_markets( + draw, + As=As, + amm_fees=amm_fees, + discounts=discounts(), + initial_prices=prices, +): + _A = draw(As) + _fee = draw(amm_fees) + _loan_discount, _liq_discount = draw(discounts) + _price = draw(initial_prices) + + proto = Llamalend(initial_price=_price) + + _borrowed_token = draw(collaterals) + _collateral_token = draw(collaterals) + + market = proto.create_lending_market( + borrowed_token=_borrowed_token, + collateral_token=_collateral_token, + A=_A, + fee=_fee, + loan_discount=_loan_discount, + liquidation_discount=_liq_discount, + price_oracle=proto.price_oracle, + name="Fuzz Vault", + min_borrow_rate=0, + max_borrow_rate=MAX_UINT256, + seed_amount=0, + ) + + note( + "deployed lend market with " + + f"A={_A}, fee={_fee}, loan_discount={_loan_discount}, liq_discount={_liq_discount}" + + f"; price={_price}" + ) + + return market + + +# TODO eventually fix this in SC (actually maybe this was for A?) +ticks = integers(min_value=MIN_TICKS + 1, max_value=MAX_TICKS) diff --git a/tests/fuzz/test_tokens_to_shrink_fuzz.py b/tests/fuzz/test_tokens_to_shrink_fuzz.py new file mode 100644 index 00000000..9cf74056 --- /dev/null +++ b/tests/fuzz/test_tokens_to_shrink_fuzz.py @@ -0,0 +1,101 @@ +import boa +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from tests.utils.constants import MIN_TICKS, WAD, ZERO_ADDRESS + + +# TODO no fixtures here +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 5000 * 10**22 + + +@given( + N=st.integers(min_value=4, max_value=50), + collateral_amount=st.integers(min_value=10**12, max_value=10**22), + p_o_frac=st.integers(min_value=0, max_value=WAD), + trade_frac=st.integers(min_value=10**15, max_value=WAD), +) +@settings(max_examples=5000) +def test_tokens_to_shrink( + price_oracle, + controller, + amm, + collateral_token, + borrowed_token, + admin, + N, + collateral_amount, + p_o_frac, + trade_frac, +): + collateral_token.approve(controller, 2**256 - 1) + borrowed_token.approve(controller, 2**256 - 1) + collateral_token.approve(amm, 2**256 - 1) + borrowed_token.approve(amm, 2**256 - 1) + + # --- Initial deposit --- + + debt = controller.max_borrowable(collateral_amount, N) + boa.deal(collateral_token, boa.env.eoa, collateral_amount) + controller.create_loan(collateral_amount, debt, N) + + # --- Change oracle price --- + + n1, n2 = amm.read_user_tick_numbers(boa.env.eoa) + p_up, p_down = amm.p_oracle_up(n1 - 2), amm.p_oracle_down(n2 + 2) + oracle_price = p_down + (p_up - p_down) * p_o_frac // WAD + oracle_price_band = n1 - 2 + while amm.p_oracle_down(oracle_price_band) > oracle_price: + oracle_price_band += 1 + + with boa.env.prank(admin): + price_oracle.set_price(oracle_price) + boa.env.time_travel(3600) + assert amm.price_oracle() == oracle_price + + # --- Trade to push user into soft-liquidation --- + + trade_amount = debt * trade_frac // WAD + boa.deal(borrowed_token, boa.env.eoa, trade_amount) + amm.exchange(0, 1, trade_amount, 0) + user_state = controller.user_state(boa.env.eoa) + active_band = amm.active_band() + + # --- Repay --- + + if n2 < active_band + MIN_TICKS: + with boa.reverts("Can't shrink"): + controller.tokens_to_shrink(boa.env.eoa) + else: + tokens_to_shrink = controller.tokens_to_shrink(boa.env.eoa) + + boa.deal(borrowed_token, boa.env.eoa, tokens_to_shrink) + controller.repay( + tokens_to_shrink, boa.env.eoa, active_band, ZERO_ADDRESS, b"", True + ) + + _n1, _n2 = amm.read_user_tick_numbers(boa.env.eoa) + + if tokens_to_shrink > 0: + assert ( + max(active_band, oracle_price_band) + 1 + <= _n1 + <= max(active_band, oracle_price_band) + 2 + ) + else: + assert _n1 > active_band + assert amm.active_band() == active_band + assert _n2 - _n1 == n2 - (active_band + 1) + + _user_state = controller.user_state(boa.env.eoa) + assert _user_state[0] == user_state[0] # collateral unchanged + assert _user_state[1] == 0 # borrowed == 0 + assert ( + _user_state[2] == user_state[2] - user_state[1] - tokens_to_shrink + ) # _debt == debt - xy[0] - tokens_to_shrink + assert _user_state[3] == min(n2 - active_band, n2 - n1 + 1) diff --git a/tests/lending/_test_bigfuzz_two.py b/tests/lending/_test_bigfuzz_two.py deleted file mode 100644 index ae3205cb..00000000 --- a/tests/lending/_test_bigfuzz_two.py +++ /dev/null @@ -1,422 +0,0 @@ -import boa -from math import log2, ceil -from boa import BoaError -from hypothesis import settings -from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule # , invariant - - -# Variables and methods to check -# * A - -# * liquidate -# * self_liquidate -# * set_debt_ceiling -# * set_borrowing_discounts -# * collect AMM fees - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -USE_FRACTION = 1 -MIN_RATE = 10**15 / (365 * 86400) # 0.1% -MAX_RATE = 10**19 / (365 * 86400) # 1000% - - -class BigFuzz(RuleBasedStateMachine): - collateral_amount = st.integers(min_value=0, max_value=10**18 * 10**6 // 3000) - loan_amount = st.integers(min_value=0, max_value=10**18 * 10**6) - n = st.integers(min_value=5, max_value=50) - ratio = st.floats(min_value=0, max_value=2) - - is_pump = st.booleans() - rate = st.integers(min_value=0, max_value=int(1e18 * 0.2 / 365 / 86400)) - oracle_step = st.floats(min_value=-0.01, max_value=0.01) - - user_id = st.integers(min_value=0, max_value=9) - liquidator_id = st.integers(min_value=0, max_value=9) - time_shift = st.integers(min_value=1, max_value=30 * 86400) - - extended_mode = st.integers(min_value=0, max_value=1) - is_short = st.booleans() - liquidate_frac = st.integers(min_value=0, max_value=10**18 + 1) - - def __init__(self): - super().__init__() - - self.mpolicy_long = self.mpolicy_interface.at(self.controller_long.monetary_policy()) - self.mpolicy_short = self.mpolicy_interface.at(self.controller_short.monetary_policy()) - - self.A = self.amm_long.A() - assert self.A == self.amm_short.A() - - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) - self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) - self.mul = {} - self.mul[False] = self.borrowed_mul - self.mul[True] = self.collateral_mul - self.tokens_b = {} - self.tokens_b[False] = self.borrowed_token - self.tokens_b[True] = self.collateral_token - self.vaults_c = {} - self.vaults_c[False] = self.vault_short - self.vaults_c[True] = self.vault_long - self.vaults = {} - self.vaults[False] = self.vault_long - self.vaults[True] = self.vault_short - self.controllers = {} - self.controllers[False] = self.controller_long - self.controllers[True] = self.controller_short - self.amms = {} - self.amms[False] = self.amm_short - self.amms[True] = self.amm_long - - for user in self.accounts: - with boa.env.prank(user): - self.borrowed_token.approve(self.vault_long.address, 2**256-1) - self.collateral_token.approve(self.vault_short.address, 2**256-1) - - self.borrowed_token.approve(self.amm_long.address, 2**256-1) - self.borrowed_token.approve(self.controller_long.address, 2**256-1) - self.vault_short.approve(self.amm_long.address, 2**256-1) - self.vault_short.approve(self.controller_long.address, 2**256-1) - - self.collateral_token.approve(self.amm_short.address, 2**256-1) - self.collateral_token.approve(self.controller_short.address, 2**256-1) - self.vault_long.approve(self.amm_short.address, 2**256-1) - self.vault_long.approve(self.controller_short.address, 2**256-1) - - # Auxiliary methods # - def check_debt_ceiling(self, amount, is_short): - return self.tokens_b.balanceOf(self.controllers[is_short].address) >= amount - - def get_max_good_band(self, is_short): - return ceil(log2(self.amms[is_short].get_base_price() / self.amms[is_short].price_oracle()) / log2(self.A / (self.A - 1)) + 5) - - @rule(uid=user_id, asset_amount=loan_amount, is_short=is_short) - def deposit_vault(self, uid, asset_amount, is_short): - asset_amount = asset_amount // self.mul[is_short] - user = self.accounts[uid] - balance = self.tokens_b[is_short].balanceOf(user) - if balance < asset_amount: - self.tokens_b[is_short]._mint_for_testing(user, asset_amount - balance) - with boa.env.prank(user): - self.vaults[is_short].deposit(asset_amount) - - @rule(uid=user_id, shares_amount=loan_amount, is_short=is_short) - def withdraw_vault(self, uid, shares_amount, is_short): - user = self.accounts[uid] - if shares_amount <= self.vaults[is_short].maxRedeem(user): - with boa.env.prank(user): - self.vaults[is_short].redeem(shares_amount) - - # Borrowing and returning # - # @rule(y=collateral_amount, n=n, uid=user_id, ratio=ratio, is_short=is_short) - def create_loan(self, y, n, ratio, uid, is_short): - debt = int(ratio * 3000 * y) // self.borrowed_mul - y = y // self.collateral_mul - user = self.accounts[uid] - with boa.env.prank(user): - self.collateral_token._mint_for_testing(user, y) - max_debt = self.market_controller.max_borrowable(y, n) - if not self.check_debt_ceiling(debt): - with boa.reverts(): - self.market_controller.create_loan(y, debt, n) - return - if (debt > max_debt or y * self.collateral_mul // n <= 100 or debt == 0 - or self.market_controller.loan_exists(user)): - if debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40)): - try: - self.market_controller.create_loan(y, debt, n) - except Exception: - pass - else: - try: - self.market_controller.create_loan(y, debt, n) - except Exception: - return - assert debt < max_debt * (self.A / (self.A - 1))**0.4 - return - else: - try: - self.market_controller.create_loan(y, debt, n) - except BoaError: - # Reverts at low numbers due to numerical issues of log calculation - # Not a problem because these numbers are not practical to use - # And it doesn't allow to create a "bad" loan - p_o = self.market_amm.price_oracle() - p = self.market_amm.get_p() - # Another reason - price increase being too large to handle without oracle following it - # XXX check of self.borrowed_mul is here also - assert y * ratio * self.collateral_mul < n * 500 or p > p_o - return - - # @rule(ratio=ratio, uid=user_id) - def repay(self, ratio, uid): - user = self.accounts[uid] - debt = self.market_controller.debt(user) - amount = int(ratio * debt) - diff = amount - self.borrowed_token.balanceOf(user) - if diff > 0: - with boa.env.prank(user): - self.borrowed_token._mint_for_testing(user, diff) - with boa.env.prank(user): - if debt == 0 and amount > 0: - with boa.reverts(): - self.market_controller.repay(amount, user) - else: - if amount > 0 and ( - (amount >= debt and (debt > self.borrowed_token.balanceOf(user) + self.market_amm.get_sum_xy(user)[0])) - or (amount < debt and (amount > self.borrowed_token.balanceOf(user)))): - with boa.reverts(): - self.market_controller.repay(amount, user) - else: - self.market_controller.repay(amount, user) - - # @rule(y=collateral_amount, uid=user_id) - def add_collateral(self, y, uid): - y = y // self.collateral_mul - user = self.accounts[uid] - exists = self.market_controller.loan_exists(user) - if exists: - n1, n2 = self.market_amm.read_user_tick_numbers(user) - n0 = self.market_amm.active_band() - self.collateral_token._mint_for_testing(user, y) - - with boa.env.prank(user): - if (exists and n1 > n0 and self.market_amm.p_oracle_up(n1) < self.market_amm.price_oracle()) or y == 0: - self.market_controller.add_collateral(y, user) - else: - with boa.reverts(): - self.market_controller.add_collateral(y, user) - - # @rule(y=collateral_amount, uid=user_id) - def remove_collateral(self, y, uid): - y = y // self.collateral_mul - user = self.accounts[uid] - user_collateral, user_borrowed, debt, N = self.market_controller.user_state(user) - if debt > 0: - n1, n2 = self.market_amm.read_user_tick_numbers(user) - n0 = self.market_amm.active_band() - - with boa.env.prank(user): - if (debt > 0 and n1 > n0) or y == 0: - before = self.collateral_token.balanceOf(user) - min_collateral = self.market_controller.min_collateral(debt, N) - try: - self.market_controller.remove_collateral(y) - except Exception: - if user_borrowed > 0: - return - if (user_collateral - y) * self.collateral_mul // N <= 100: - return - if user_collateral - y > min_collateral: - raise - else: - return - after = self.collateral_token.balanceOf(user) - assert after - before == y - else: - with boa.reverts(): - self.market_controller.remove_collateral(y) - - # @rule(y=collateral_amount, uid=user_id, ratio=ratio) - def borrow_more(self, y, ratio, uid): - y = y // self.collateral_mul - user = self.accounts[uid] - self.collateral_token._mint_for_testing(user, y) - - with boa.env.prank(user): - if not self.market_controller.loan_exists(user): - with boa.reverts(): - self.market_controller.borrow_more(y, 1) - - else: - sx, sy = self.market_amm.get_sum_xy(user) - n1, n2 = self.market_amm.read_user_tick_numbers(user) - n = n2 - n1 + 1 - amount = int(self.market_amm.price_oracle() * (sy + y) * self.collateral_mul / 1e18 * ratio / self.borrowed_mul) - current_debt = self.market_controller.debt(user) - final_debt = current_debt + amount - - if not self.check_debt_ceiling(amount) and amount > 0: - with boa.reverts(): - self.market_controller.borrow_more(y, amount) - return - - if sx == 0 or amount == 0: - max_debt = self.market_controller.max_borrowable(sy + y, n, current_debt) - if final_debt > max_debt and amount > 0: - # XXX any borrowed_mul here? - if final_debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40) - 1e-9): - try: - self.market_controller.borrow_more(y, amount) - except Exception: - pass - else: - with boa.reverts(): - self.market_controller.borrow_more(y, amount) - else: - try: - self.market_controller.borrow_more(y, amount) - except Exception: - if self.get_max_good_band() > self.market_amm.active_band_with_skip(): - # Otherwise (if price desync is too large) - this fail is to be expected - raise - - else: - with boa.reverts(): - self.market_controller.borrow_more(y, amount) - - # Trading - def trade_to_price(self, p): - user = self.accounts[0] - with boa.env.prank(user): - amount, is_pump = self.market_amm.get_amount_for_price(p) - if amount > 0: - if is_pump: - self.borrowed_token._mint_for_testing(user, amount) - self.market_amm.exchange(0, 1, amount, 0) - else: - self.collateral_token._mint_for_testing(user, amount) - self.market_amm.exchange(1, 0, amount, 0) - - # @rule(r=ratio, is_pump=is_pump, uid=user_id) - def trade(self, r, is_pump, uid): - user = self.accounts[uid] - with boa.env.prank(user): - if is_pump: - amount = int(r * self.borrowed_token.totalSupply()) - self.borrowed_token._mint_for_testing(user, amount) - self.market_amm.exchange(0, 1, amount, 0) - else: - amount = int(r * self.collateral_token.totalSupply()) - self.collateral_token._mint_for_testing(user, amount) - self.market_amm.exchange(1, 0, amount, 0) - - # @rule(emode=extended_mode, frac=liquidate_frac) - def self_liquidate_and_health(self, emode, frac): - for user in self.accounts: - try: - health = self.market_controller.health(user) - except BoaError: - # Too deep - return - if self.market_controller.loan_exists(user) and health <= 0: - with boa.env.prank(user): - debt = self.market_controller.debt(user) - diff = debt - self.borrowed_token.balanceOf(user) - if diff > 0: - self.borrowed_token._mint_for_testing(user, diff) - if emode == USE_FRACTION: - try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) - except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: - return - raise - else: - self.market_controller.liquidate(user, 0) - if emode == 0 or frac == 10**18: - assert not self.market_controller.loan_exists(user) - with boa.reverts(): - self.market_controller.health(user) - - # @rule(uid=user_id, luid=liquidator_id, emode=extended_mode, frac=liquidate_frac) - def liquidate(self, uid, luid, emode, frac): - user = self.accounts[uid] - liquidator = self.accounts[luid] - if user == liquidator: - return # self-liquidate tested separately - - with boa.env.prank(liquidator): - self.fake_leverage.approve_all() - - if not self.market_controller.loan_exists(user): - debt = self.market_controller.debt(user) - diff = debt - self.borrowed_token.balanceOf(user) - if diff > 0: - self.borrowed_token._mint_for_testing(liquidator, diff) - - with boa.env.prank(liquidator): - with boa.reverts(): - if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) - else: - self.market_controller.liquidate(user, 0) - else: - health_limit = self.market_controller.liquidation_discount() - try: - health = self.market_controller.health(user, True) - except Exception as e: - assert 'Too deep' in str(e) - with boa.env.prank(liquidator): - if health >= health_limit: - with boa.reverts(): - if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) - else: - self.market_controller.liquidate(user, 0) - else: - if emode == USE_FRACTION: - try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) - except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: - return - raise - else: - self.market_controller.liquidate(user, 0) - if emode == 0 or frac == 10**18: - with boa.reverts(): - self.market_controller.health(user) - - # Other - # @rule(dp=oracle_step) - def shift_oracle(self, dp): - # Oracle shift is done via adiabatic trading which shouldn't decrease health - if dp != 0: - p0 = self.price_oracle.price() - self.trade_to_price(p0) - p = int(p0 * (1 + dp)) - with boa.env.prank(self.admin): - self.price_oracle.set_price(p) - self.trade_to_price(p) - - # @rule(min_rate=rate, max_rate=rate) - def rule_change_rate(self, min_rate, max_rate): - with boa.env.prank(self.admin): - if min_rate > max_rate or min(min_rate, max_rate) < MIN_RATE or max(min_rate, max_rate) > MAX_RATE: - with boa.reverts(): - self.market_mpolicy.set_rates(min_rate, max_rate) - else: - self.market_mpolicy.set_rates(min_rate, max_rate) - - # @rule(dt=time_shift) - def time_travel(self, dt): - boa.env.time_travel(dt) - - # @invariant() - def debt_supply(self): - total_debt = self.market_controller.total_debt() - if total_debt == 0: - assert abs(self.market_controller.minted() - self.market_controller.redeemed()) <= 1 - assert abs(sum(self.market_controller.debt(u) for u in self.accounts) - total_debt) <= 10 - - # @invariant() - def minted_redeemed(self): - assert self.market_controller.redeemed() + self.market_controller.total_debt() >= self.market_controller.minted() - - -def test_big_fuzz( - vault_long, vault_short, borrowed_token, collateral_token, - amm_long, amm_short, controller_long, controller_short, - price_oracle, mpolicy_interface): - BigFuzz.TestCase.settings = settings(max_examples=200, stateful_step_count=20) - # Or quick check - # BigFuzz.TestCase.settings = settings(max_examples=25, stateful_step_count=20) - for k, v in locals().items(): - setattr(BigFuzz, k, v) - run_state_machine_as_test(BigFuzz) diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index eeaa3bb2..2d9276b4 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -1,219 +1,31 @@ import boa import pytest -from itertools import product - - -@pytest.fixture(scope="session") -def amm_interface(): - return boa.load_partial('contracts/AMM.vy') - - -@pytest.fixture(scope="session") -def amm_impl(amm_interface, admin): - with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def controller_interface(): - return boa.load_partial('contracts/Controller.vy') - - -@pytest.fixture(scope="session") -def controller_impl(controller_interface, admin): - with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="module") -def stablecoin(get_borrowed_token): - return get_borrowed_token(18) - - -@pytest.fixture(scope="session") -def vault_impl(admin): - with boa.env.prank(admin): - return boa.load('contracts/lending/Vault.vy') - - -@pytest.fixture(scope="session") -def price_oracle_interface(): - return boa.load_partial('contracts/price_oracles/CryptoFromPool.vy') - - -@pytest.fixture(scope="session") -def price_oracle_impl(price_oracle_interface, admin): - with boa.env.prank(admin): - return price_oracle_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def mpolicy_interface(): - return boa.load_partial('contracts/mpolicies/SemilogMonetaryPolicy.vy') - - -@pytest.fixture(scope="session") -def mpolicy_impl(mpolicy_interface, admin): - with boa.env.prank(admin): - return mpolicy_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def gauge_impl(admin): - with boa.env.prank(admin): - return boa.load_partial('contracts/testing/MockGauge.vy').deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def factory_partial(): - return boa.load_partial('contracts/lending/OneWayLendingFactory.vy') +from tests.utils.deployers import FAKE_LEVERAGE_DEPLOYER +from tests.utils.deployers import SEMILOG_MONETARY_POLICY_DEPLOYER @pytest.fixture(scope="module") -def factory(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, gauge_impl, admin): - with boa.env.prank(admin): - return factory_partial.deploy( - stablecoin.address, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, gauge_impl, - admin) - - -@pytest.fixture(scope="module", params=product([2, 6, 8, 18], [True, False])) -def tokens_for_vault(get_collateral_token, stablecoin, request): - decimals, stablecoin_is_borrowed = request.param - token = get_collateral_token(decimals) - if stablecoin_is_borrowed: - borrowed_token = stablecoin - collateral_token = token - else: - borrowed_token = token - collateral_token = stablecoin - return borrowed_token, collateral_token +def market_type(): + """Force lending-only markets for tests in this folder.""" + return "lending" @pytest.fixture(scope="module") -def collateral_token(tokens_for_vault): - return tokens_for_vault[1] +def lending_monetary_policy(): + """Override lending policy to Semilog for all tests in this folder.""" + return SEMILOG_MONETARY_POLICY_DEPLOYER @pytest.fixture(scope="module") -def borrowed_token(tokens_for_vault): - return tokens_for_vault[0] - - -@pytest.fixture(scope="module") -def vault(factory, vault_impl, collateral_token, borrowed_token, price_oracle, admin): +def fake_leverage(collateral_token, borrowed_token, controller, admin): with boa.env.prank(admin): - return vault_impl.at( - factory.create( - borrowed_token.address, collateral_token.address, - 100, int(0.006 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), - price_oracle.address, "Test vault" - ) + leverage = FAKE_LEVERAGE_DEPLOYER.deploy( + borrowed_token.address, + collateral_token.address, + controller.address, + 3000 * 10**18, + ) + boa.deal( + collateral_token, leverage.address, 1000 * 10 ** collateral_token.decimals() ) - - -@pytest.fixture(scope="module") -def market_controller(vault, controller_interface): - return controller_interface.at(vault.controller()) - - -@pytest.fixture(scope="module") -def market_amm(vault, amm_interface): - return amm_interface.at(vault.amm()) - - -@pytest.fixture(scope="module") -def market_mpolicy(market_controller, mpolicy_interface): - return mpolicy_interface.at(market_controller.monetary_policy()) - - -@pytest.fixture(scope="session") -def mock_token_interface(): - return boa.load_partial('contracts/testing/ERC20Mock.vy') - - -@pytest.fixture(scope="module") -def filled_controller(vault, borrowed_token, market_controller, admin): - with boa.env.prank(admin): - amount = 100 * 10**6 * 10**(borrowed_token.decimals()) - borrowed_token._mint_for_testing(admin, amount) - borrowed_token.approve(vault.address, 2**256 - 1) - vault.deposit(amount) - return market_controller - - -@pytest.fixture(scope="module") -def fake_leverage(collateral_token, borrowed_token, market_controller, admin): - with boa.env.prank(admin): - leverage = boa.load('contracts/testing/FakeLeverage.vy', borrowed_token.address, collateral_token.address, - market_controller.address, 3000 * 10**18) - collateral_token._mint_for_testing(leverage.address, 1000 * 10**collateral_token.decimals()) return leverage - - -@pytest.fixture(scope="session") -def vault_price_oracle_impl(admin): - with boa.env.prank(admin): - return boa.load_partial('contracts/price_oracles/CryptoFromPoolVault.vy').deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def wrapper_oracle_impl(admin): - with boa.env.prank(admin): - return boa.load_partial('contracts/price_oracles/OracleVaultWrapper.vy').deploy_as_blueprint() - - -@pytest.fixture(scope="session") -def factory_2way_partial(): - return boa.load_partial('contracts/lending/TwoWayLendingFactory.vy') - - -@pytest.fixture(scope="module") -def factory_2way(factory_2way_partial, stablecoin, amm_impl, controller_impl, vault_impl, vault_price_oracle_impl, wrapper_oracle_impl, - mpolicy_impl, gauge_impl, admin): - with boa.env.prank(admin): - return factory_2way_partial.deploy( - stablecoin, amm_impl, controller_impl, vault_impl, - vault_price_oracle_impl, wrapper_oracle_impl, mpolicy_impl, gauge_impl, admin) - - -@pytest.fixture(scope="module") -def vaults_2way(factory_2way, vault_impl, collateral_token, borrowed_token, price_oracle, admin): - with boa.env.prank(admin): - vault_long, vault_short = factory_2way.create( - borrowed_token.address, collateral_token.address, - 100, int(0.006 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), - price_oracle.address, "Test vault") - return vault_impl.at(vault_long), vault_impl.at(vault_short) - - -@pytest.fixture(scope="module") -def vault_long(vaults_2way): - return vaults_2way[0] - - -@pytest.fixture(scope="module") -def vault_short(vaults_2way): - return vaults_2way[1] - - -@pytest.fixture(scope="module") -def controller_long(vault_long, controller_interface): - return controller_interface.at(vault_long.controller()) - - -@pytest.fixture(scope="module") -def amm_long(vault_long, amm_interface): - return amm_interface.at(vault_long.amm()) - - -@pytest.fixture(scope="module") -def controller_short(vault_short, controller_interface): - return controller_interface.at(vault_short.controller()) - - -@pytest.fixture(scope="module") -def amm_short(vault_short, amm_interface): - return amm_interface.at(vault_short.amm()) diff --git a/tests/lending/test_bigfuzz.py b/tests/lending/test_bigfuzz.py index bfe79709..87f9cc1b 100644 --- a/tests/lending/test_bigfuzz.py +++ b/tests/lending/test_bigfuzz.py @@ -1,9 +1,20 @@ import boa +import pytest from math import log2, ceil from boa import BoaError from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) + +from tests.utils.constants import ZERO_ADDRESS + + +pytestmark = pytest.mark.xfail(strict=True, reason="stateful fuzz currently unstable") # Variables and methods to check @@ -15,8 +26,6 @@ # * set_debt_ceiling # * set_borrowing_discounts # * collect AMM fees - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" USE_FRACTION = 1 USE_CALLBACKS = 2 MIN_RATE = 10**15 / (365 * 86400) # 0.1% @@ -42,23 +51,27 @@ class BigFuzz(RuleBasedStateMachine): def __init__(self): super().__init__() - self.A = self.market_amm.A() - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) - self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) + self.A = self.amm.A() + self.collateral_mul = 10 ** (18 - self.collateral_token.decimals()) + self.borrowed_mul = 10 ** (18 - self.borrowed_token.decimals()) for user in self.accounts: with boa.env.prank(user): - self.borrowed_token.approve(self.vault.address, 2**256-1) - self.borrowed_token.approve(self.market_amm.address, 2**256-1) - self.borrowed_token.approve(self.market_controller.address, 2**256-1) - self.collateral_token.approve(self.market_amm.address, 2**256-1) - self.collateral_token.approve(self.market_controller.address, 2**256-1) + self.borrowed_token.approve(self.vault.address, 2**256 - 1) + self.borrowed_token.approve(self.amm.address, 2**256 - 1) + self.borrowed_token.approve(self.controller.address, 2**256 - 1) + self.collateral_token.approve(self.amm.address, 2**256 - 1) + self.collateral_token.approve(self.controller.address, 2**256 - 1) # Auxiliary methods # def check_debt_ceiling(self, amount): - return self.borrowed_token.balanceOf(self.market_controller.address) >= amount + return self.borrowed_token.balanceOf(self.controller.address) >= amount def get_max_good_band(self): - return ceil(log2(self.market_amm.get_base_price() / self.market_amm.price_oracle()) / log2(self.A / (self.A - 1)) + 5) + return ceil( + log2(self.amm.get_base_price() / self.amm.price_oracle()) + / log2(self.A / (self.A - 1)) + + 5 + ) @rule(uid=user_id, asset_amount=loan_amount) def deposit_vault(self, uid, asset_amount): @@ -66,7 +79,7 @@ def deposit_vault(self, uid, asset_amount): user = self.accounts[uid] balance = self.borrowed_token.balanceOf(user) if balance < asset_amount: - self.borrowed_token._mint_for_testing(user, asset_amount - balance) + boa.deal(self.borrowed_token, user, asset_amount - balance) with boa.env.prank(user): if self.vault.totalAssets() + asset_amount < 10000: with boa.reverts(): @@ -79,7 +92,9 @@ def withdraw_vault(self, uid, shares_amount): user = self.accounts[uid] if shares_amount <= self.vault.maxRedeem(user): with boa.env.prank(user): - expected_assets = self.vault.totalAssets() - self.vault.previewRedeem(shares_amount) + expected_assets = self.vault.totalAssets() - self.vault.previewRedeem( + shares_amount + ) if expected_assets < 10000 and expected_assets != 0: with boa.reverts(): self.vault.redeem(shares_amount) @@ -93,35 +108,39 @@ def create_loan(self, y, n, ratio, uid): y = y // self.collateral_mul user = self.accounts[uid] with boa.env.prank(user): - self.collateral_token._mint_for_testing(user, y) - max_debt = self.market_controller.max_borrowable(y, n) + boa.deal(self.collateral_token, user, y) + max_debt = self.controller.max_borrowable(y, n) if not self.check_debt_ceiling(debt): with boa.reverts(): - self.market_controller.create_loan(y, debt, n) + self.controller.create_loan(y, debt, n) return - if (debt > max_debt or y * self.collateral_mul // n <= 100 or debt == 0 - or self.market_controller.loan_exists(user)): - if debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40)): + if ( + debt > max_debt + or y * self.collateral_mul // n <= 100 + or debt == 0 + or self.controller.loan_exists(user) + ): + if debt < max_debt / (0.9999 - 20 / (y * self.collateral_mul + 40)): try: - self.market_controller.create_loan(y, debt, n) + self.controller.create_loan(y, debt, n) except Exception: pass else: try: - self.market_controller.create_loan(y, debt, n) + self.controller.create_loan(y, debt, n) except Exception: return - assert debt < max_debt * (self.A / (self.A - 1))**0.4 + assert debt < max_debt * (self.A / (self.A - 1)) ** 0.4 return else: try: - self.market_controller.create_loan(y, debt, n) + self.controller.create_loan(y, debt, n) except BoaError: # Reverts at low numbers due to numerical issues of log calculation # Not a problem because these numbers are not practical to use # And it doesn't allow to create a "bad" loan - p_o = self.market_amm.price_oracle() - p = self.market_amm.get_p() + p_o = self.amm.price_oracle() + p = self.amm.get_p() # Another reason - price increase being too large to handle without oracle following it # XXX check of self.borrowed_mul is here also assert y * ratio * self.collateral_mul < n * 500 or p > p_o @@ -130,60 +149,76 @@ def create_loan(self, y, n, ratio, uid): @rule(ratio=ratio, uid=user_id) def repay(self, ratio, uid): user = self.accounts[uid] - debt = self.market_controller.debt(user) + debt = self.controller.debt(user) amount = int(ratio * debt) + if amount == 0: + return diff = amount - self.borrowed_token.balanceOf(user) if diff > 0: with boa.env.prank(user): - self.borrowed_token._mint_for_testing(user, diff) + boa.deal(self.borrowed_token, user, diff) with boa.env.prank(user): if debt == 0 and amount > 0: with boa.reverts(): - self.market_controller.repay(amount, user) + self.controller.repay(amount, user) else: if amount > 0 and ( - (amount >= debt and (debt > self.borrowed_token.balanceOf(user) + self.market_amm.get_sum_xy(user)[0])) - or (amount < debt and (amount > self.borrowed_token.balanceOf(user)))): + ( + amount >= debt + and ( + debt + > self.borrowed_token.balanceOf(user) + + self.amm.get_sum_xy(user)[0] + ) + ) + or ( + amount < debt and (amount > self.borrowed_token.balanceOf(user)) + ) + ): with boa.reverts(): - self.market_controller.repay(amount, user) + self.controller.repay(amount, user) else: - self.market_controller.repay(amount, user) + self.controller.repay(amount, user) @rule(y=collateral_amount, uid=user_id) def add_collateral(self, y, uid): y = y // self.collateral_mul user = self.accounts[uid] - exists = self.market_controller.loan_exists(user) + exists = self.controller.loan_exists(user) if exists: - n1, n2 = self.market_amm.read_user_tick_numbers(user) - n0 = self.market_amm.active_band() - self.collateral_token._mint_for_testing(user, y) + n1, n2 = self.amm.read_user_tick_numbers(user) + n0 = self.amm.active_band() + boa.deal(self.collateral_token, user, y) with boa.env.prank(user): - if (exists and n1 > n0 and self.market_amm.p_oracle_up(n1) < self.market_amm.price_oracle()) or y == 0: - self.market_controller.add_collateral(y, user) + if ( + exists + and n1 > n0 + and self.amm.p_oracle_up(n1) < self.amm.price_oracle() + ) or y == 0: + self.controller.add_collateral(y, user) else: with boa.reverts(): - self.market_controller.add_collateral(y, user) + self.controller.add_collateral(y, user) @rule(y=collateral_amount, uid=user_id) def remove_collateral(self, y, uid): y = y // self.collateral_mul user = self.accounts[uid] - user_collateral, user_borrowed, debt, N = self.market_controller.user_state(user) + user_collateral, user_borrowed, debt, N = self.controller.user_state(user) if debt > 0: - n1, n2 = self.market_amm.read_user_tick_numbers(user) - n0 = self.market_amm.active_band() + n1, n2 = self.amm.read_user_tick_numbers(user) + n0 = self.amm.active_band() with boa.env.prank(user): if (debt > 0 and n1 > n0) or y == 0: before = self.collateral_token.balanceOf(user) if debt > 0: - min_collateral = self.market_controller.min_collateral(debt, N) + min_collateral = self.controller.min_collateral(debt, N) else: return try: - self.market_controller.remove_collateral(y) + self.controller.remove_collateral(y) except Exception: if user_borrowed > 0: return @@ -197,68 +232,80 @@ def remove_collateral(self, y, uid): assert after - before == y else: with boa.reverts(): - self.market_controller.remove_collateral(y) + self.controller.remove_collateral(y) @rule(y=collateral_amount, uid=user_id, ratio=ratio) def borrow_more(self, y, ratio, uid): y = y // self.collateral_mul user = self.accounts[uid] - self.collateral_token._mint_for_testing(user, y) + boa.deal(self.collateral_token, user, y) with boa.env.prank(user): - if not self.market_controller.loan_exists(user): + if not self.controller.loan_exists(user): with boa.reverts(): - self.market_controller.borrow_more(y, 1) + self.controller.borrow_more(y, 1) else: - sx, sy = self.market_amm.get_sum_xy(user) - n1, n2 = self.market_amm.read_user_tick_numbers(user) + sx, sy = self.amm.get_sum_xy(user) + n1, n2 = self.amm.read_user_tick_numbers(user) n = n2 - n1 + 1 - amount = int(self.market_amm.price_oracle() * (sy + y) * self.collateral_mul / 1e18 * ratio / self.borrowed_mul) - current_debt = self.market_controller.debt(user) + amount = int( + self.amm.price_oracle() + * (sy + y) + * self.collateral_mul + / 1e18 + * ratio + / self.borrowed_mul + ) + current_debt = self.controller.debt(user) final_debt = current_debt + amount if not self.check_debt_ceiling(amount) and amount > 0: with boa.reverts(): - self.market_controller.borrow_more(y, amount) + self.controller.borrow_more(y, amount) return if sx == 0 or amount == 0: - max_debt = self.market_controller.max_borrowable(sy + y, n, current_debt) + max_debt = self.controller.max_borrowable(sy + y, n, current_debt) if final_debt > max_debt and amount > 0: # XXX any borrowed_mul here? - if final_debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40) - 1e-9): + if final_debt < max_debt / ( + 0.9999 - 20 / (y * self.collateral_mul + 40) - 1e-9 + ): try: - self.market_controller.borrow_more(y, amount) + self.controller.borrow_more(y, amount) except Exception: pass else: with boa.reverts(): - self.market_controller.borrow_more(y, amount) + self.controller.borrow_more(y, amount) else: try: - self.market_controller.borrow_more(y, amount) + self.controller.borrow_more(y, amount) except Exception: - if self.get_max_good_band() > self.market_amm.active_band_with_skip(): + if ( + self.get_max_good_band() + > self.amm.active_band_with_skip() + ): # Otherwise (if price desync is too large) - this fail is to be expected raise else: with boa.reverts(): - self.market_controller.borrow_more(y, amount) + self.controller.borrow_more(y, amount) # Trading def trade_to_price(self, p): user = self.accounts[0] with boa.env.prank(user): - amount, is_pump = self.market_amm.get_amount_for_price(p) + amount, is_pump = self.amm.get_amount_for_price(p) if amount > 0: if is_pump: - self.borrowed_token._mint_for_testing(user, amount) - self.market_amm.exchange(0, 1, amount, 0) + boa.deal(self.borrowed_token, user, amount) + self.amm.exchange(0, 1, amount, 0) else: - self.collateral_token._mint_for_testing(user, amount) - self.market_amm.exchange(1, 0, amount, 0) + boa.deal(self.collateral_token, user, amount) + self.amm.exchange(1, 0, amount, 0) @rule(r=ratio, is_pump=is_pump, uid=user_id) def trade(self, r, is_pump, uid): @@ -266,51 +313,53 @@ def trade(self, r, is_pump, uid): with boa.env.prank(user): if is_pump: amount = int(r * self.borrowed_token.totalSupply()) - self.borrowed_token._mint_for_testing(user, amount) - self.market_amm.exchange(0, 1, amount, 0) + boa.deal(self.borrowed_token, user, amount) + self.amm.exchange(0, 1, amount, 0) else: amount = int(r * self.collateral_token.totalSupply()) - self.collateral_token._mint_for_testing(user, amount) - self.market_amm.exchange(1, 0, amount, 0) + boa.deal(self.collateral_token, user, amount) + self.amm.exchange(1, 0, amount, 0) @rule(emode=extended_mode, frac=liquidate_frac) def self_liquidate_and_health(self, emode, frac): for user in self.accounts: try: - health = self.market_controller.health(user) + health = self.controller.health(user) except BoaError: # Too deep return - if self.market_controller.loan_exists(user) and health <= 0: + if self.controller.loan_exists(user) and health <= 0: with boa.env.prank(user): - debt = self.market_controller.debt(user) + debt = self.controller.debt(user) diff = debt - self.borrowed_token.balanceOf(user) if diff > 0: - self.borrowed_token._mint_for_testing(user, diff) + boa.deal(self.borrowed_token, user, diff) if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: + if self.controller.debt(user) * frac // 10**18 == 0: return raise elif emode == USE_CALLBACKS: - self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) + self.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) try: - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: + if self.controller.debt(user) * frac // 10**18 == 0: return raise else: - self.market_controller.liquidate(user, 0) + self.controller.liquidate(user, 0) if emode == 0 or frac == 10**18: - assert not self.market_controller.loan_exists(user) + assert not self.controller.loan_exists(user) with boa.reverts(): - self.market_controller.health(user) + self.controller.health(user) @rule(uid=user_id, luid=liquidator_id, emode=extended_mode, frac=liquidate_frac) def liquidate(self, uid, luid, emode, frac): @@ -322,73 +371,85 @@ def liquidate(self, uid, luid, emode, frac): with boa.env.prank(liquidator): self.fake_leverage.approve_all() - if not self.market_controller.loan_exists(user): - debt = self.market_controller.debt(user) + if not self.controller.loan_exists(user): + debt = self.controller.debt(user) diff = debt - self.borrowed_token.balanceOf(user) if diff > 0: - self.borrowed_token._mint_for_testing(liquidator, diff) + boa.deal(self.borrowed_token, liquidator, diff) with boa.env.prank(liquidator): with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") elif emode == USE_CALLBACKS: - self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) + self.controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) else: - self.market_controller.liquidate(user, 0) + self.controller.liquidate(user, 0) if emode == USE_CALLBACKS: - self.borrowed_token.transferFrom(self.fake_leverage.address, liquidator, - self.borrowed_token.balanceOf(self.fake_leverage.address)) + self.borrowed_token.transferFrom( + self.fake_leverage.address, + liquidator, + self.borrowed_token.balanceOf(self.fake_leverage.address), + ) else: - health_limit = self.market_controller.liquidation_discount() + health_limit = self.controller.liquidation_discount() try: - health = self.market_controller.health(user, True) + health = self.controller.health(user, True) except Exception as e: - assert 'Too deep' in str(e) + assert "Too deep" in str(e) with boa.env.prank(liquidator): if health >= health_limit: with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") elif emode == USE_CALLBACKS: - self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) + self.controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) else: - self.market_controller.liquidate(user, 0) + self.controller.liquidate(user, 0) if emode == USE_CALLBACKS: - self.borrowed_token.transferFrom(self.fake_leverage.address, liquidator, - self.borrowed_token.balanceOf(self.fake_leverage.address)) + self.borrowed_token.transferFrom( + self.fake_leverage.address, + liquidator, + self.borrowed_token.balanceOf(self.fake_leverage.address), + ) else: if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.controller.liquidate(user, 0, frac, ZERO_ADDRESS, b"") except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: + if self.controller.debt(user) * frac // 10**18 == 0: return raise elif emode == USE_CALLBACKS: - self.borrowed_token.transfer(self.fake_leverage.address, self.borrowed_token.balanceOf(user)) + self.borrowed_token.transfer( + self.fake_leverage.address, + self.borrowed_token.balanceOf(user), + ) try: - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: - if self.market_controller.debt(user) * frac // 10**18 == 0: + if self.controller.debt(user) * frac // 10**18 == 0: return raise else: - self.market_controller.liquidate(user, 0) + self.controller.liquidate(user, 0) if emode == 0 or frac == 10**18: with boa.reverts(): - self.market_controller.health(user) + self.controller.health(user) # Other @rule(dp=oracle_step) @@ -405,11 +466,15 @@ def shift_oracle(self, dp): @rule(min_rate=rate, max_rate=rate) def rule_change_rate(self, min_rate, max_rate): with boa.env.prank(self.admin): - if min_rate > max_rate or min(min_rate, max_rate) < MIN_RATE or max(min_rate, max_rate) > MAX_RATE: + if ( + min_rate > max_rate + or min(min_rate, max_rate) < MIN_RATE + or max(min_rate, max_rate) > MAX_RATE + ): with boa.reverts(): - self.market_mpolicy.set_rates(min_rate, max_rate) + self.monetary_policy.set_rates(min_rate, max_rate) else: - self.market_mpolicy.set_rates(min_rate, max_rate) + self.monetary_policy.set_rates(min_rate, max_rate) @rule(dt=time_shift) def time_travel(self, dt): @@ -417,19 +482,35 @@ def time_travel(self, dt): @invariant() def debt_supply(self): - total_debt = self.market_controller.total_debt() + total_debt = self.controller.total_debt() if total_debt == 0: - assert self.market_controller.minted() <= self.market_controller.redeemed() # Paid back more than lent out - assert abs(sum(self.market_controller.debt(u) for u in self.accounts) - total_debt) <= 10 + assert ( + self.controller.lent() <= self.controller.repaid() + ) # Paid back more than lent out + assert ( + abs(sum(self.controller.debt(u) for u in self.accounts) - total_debt) <= 10 + ) @invariant() def minted_redeemed(self): - assert self.market_controller.redeemed() + self.market_controller.total_debt() >= self.market_controller.minted() + assert ( + self.controller.repaid() + self.controller.total_debt() + >= self.controller.lent() + ) def test_big_fuzz( - vault, borrowed_token, collateral_token, market_mpolicy, accounts, admin, market_amm, market_controller, - price_oracle, fake_leverage): + vault, + borrowed_token, + collateral_token, + monetary_policy, + accounts, + admin, + amm, + controller, + price_oracle, + fake_leverage, +): BigFuzz.TestCase.settings = settings(max_examples=2000, stateful_step_count=20) # Or quick check # BigFuzz.TestCase.settings = settings(max_examples=25, stateful_step_count=20) diff --git a/tests/lending/test_fuzz_max_borrowable.py b/tests/lending/test_fuzz_max_borrowable.py new file mode 100644 index 00000000..63224c1c --- /dev/null +++ b/tests/lending/test_fuzz_max_borrowable.py @@ -0,0 +1,95 @@ +import boa +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + + +from tests.utils.constants import DEAD_SHARES + + +@given( + collateral_amount=st.integers(min_value=100, max_value=10**20), + n=st.integers(min_value=4, max_value=50), + f_p_o=st.floats(min_value=0.7, max_value=1.3), +) +@settings(max_examples=1000) +def test_max_borrowable( + borrowed_token, + collateral_token, + amm, + controller, + price_oracle, + admin, + collateral_amount, + n, + f_p_o, +): + # Create some liquidity and go into the band + with boa.env.prank(admin): + c_amount = 10 ** collateral_token.decimals() + boa.deal(collateral_token, admin, c_amount) + collateral_token.approve(controller, 2**256 - 1) + controller.create_loan(c_amount, controller.max_borrowable(c_amount, 5), 5) + borrowed_token.approve(amm.address, 2**256 - 1) + amm.exchange(0, 1, 100, 0) + + # Change oracle + p_o = int(price_oracle.price() * f_p_o) + with boa.env.prank(admin): + price_oracle.set_price(p_o) + + max_borrowable = controller.max_borrowable(collateral_amount, n) + total = borrowed_token.balanceOf(controller.address) + if max_borrowable >= total: + return + with boa.reverts(): + controller.calculate_debt_n1( + collateral_amount, int(max_borrowable * 1.001) + n, n + ) + if max_borrowable == 0: + return + controller.calculate_debt_n1(collateral_amount, max_borrowable, n) + + min_collateral = controller.min_collateral(max_borrowable, n) + assert min_collateral == pytest.approx( + collateral_amount, + rel=1e-6 + + (n**2 + n * DEAD_SHARES) + * (1 / min(min_collateral, collateral_amount) + 1 / max_borrowable), + ) + + +@given( + debt_amount=st.integers(min_value=100, max_value=10**20), + n=st.integers(min_value=4, max_value=50), + f_p_o=st.floats(min_value=0.7, max_value=1.3), +) +@settings(max_examples=1000) +def test_min_collateral( + borrowed_token, + collateral_token, + amm, + controller, + price_oracle, + admin, + debt_amount, + n, + f_p_o, +): + # Create some liquidity and go into the band + with boa.env.prank(admin): + c_amount = 10 ** collateral_token.decimals() + boa.deal(collateral_token, admin, c_amount) + collateral_token.approve(controller, 2**256 - 1) + controller.create_loan(c_amount, controller.max_borrowable(c_amount, 5), 5) + borrowed_token.approve(amm.address, 2**256 - 1) + amm.exchange(0, 1, 10**2, 0) + + # Change oracle + p_o = int(price_oracle.price() * f_p_o) + with boa.env.prank(admin): + price_oracle.set_price(p_o) + + min_collateral = controller.min_collateral(debt_amount, n) + + controller.calculate_debt_n1(min_collateral, debt_amount, n) diff --git a/tests/lending/test_health_calculator_create.py b/tests/lending/test_health_calculator_create.py index 887b60da..47cb5913 100644 --- a/tests/lending/test_health_calculator_create.py +++ b/tests/lending/test_health_calculator_create.py @@ -8,24 +8,26 @@ debt=st.integers(min_value=10**10, max_value=2 * 10**6 * 10**18), collateral=st.integers(min_value=10**10, max_value=10**9 * 10**18 // 3000), ) -def test_health_calculator_create(market_amm, filled_controller, collateral_token, collateral, debt, n, accounts): - collateral = collateral // 10**(18 - collateral_token.decimals()) +def test_health_calculator_create( + amm, controller, collateral_token, collateral, debt, n, accounts +): + collateral = collateral // 10 ** (18 - collateral_token.decimals()) user = accounts[1] calculator_fail = False try: - health = filled_controller.health_calculator(user, collateral, debt, False, n) - health_full = filled_controller.health_calculator(user, collateral, debt, True, n) + health = controller.health_calculator(user, collateral, debt, False, n) + health_full = controller.health_calculator(user, collateral, debt, True, n) except Exception: calculator_fail = True - collateral_token._mint_for_testing(user, collateral) + boa.deal(collateral_token, user, collateral) with boa.env.prank(user): try: - filled_controller.create_loan(collateral, debt, n) + controller.create_loan(collateral, debt, n) except Exception: return assert not calculator_fail - assert abs(filled_controller.health(user) - health) / 1e18 < n * 2e-5 - assert abs(filled_controller.health(user, True) - health_full) / 1e18 < n * 2e-5 + assert abs(controller.health(user) - health) / 1e18 < n * 2e-5 + assert abs(controller.health(user, True) - health_full) / 1e18 < n * 2e-5 diff --git a/tests/lending/test_health_calculator_stateful.py b/tests/lending/test_health_calculator_stateful.py index 20188421..77fbaaed 100644 --- a/tests/lending/test_health_calculator_stateful.py +++ b/tests/lending/test_health_calculator_stateful.py @@ -1,15 +1,21 @@ """ Stateful test to create and repay loans without moving the price oracle """ + import boa from contextlib import contextmanager from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant -from ..conftest import approx +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) +import pytest -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class AllGood(Exception): @@ -19,21 +25,24 @@ class AllGood(Exception): class StatefulLendBorrow(RuleBasedStateMachine): n = st.integers(min_value=5, max_value=50) amount_frac = st.floats(min_value=0.01, max_value=2) - c_amount = st.integers(min_value=10**13, max_value=2**128-1) + c_amount = st.integers(min_value=10**13, max_value=2**128 - 1) user_id = st.integers(min_value=0, max_value=9) def __init__(self): super().__init__() - self.controller = self.filled_controller - self.amm = self.market_amm - self.debt_ceiling = self.borrowed_token.balanceOf(self.controller) - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) - self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) - self.preexisting_supply = self.borrowed_token.totalSupply() - self.borrowed_token.balanceOf(self.controller) + self.controller = self.controller + self.amm = self.amm + self.debt_ceiling = self.controller.available_balance() + self.collateral_mul = 10 ** (18 - self.collateral_token.decimals()) + self.borrowed_mul = 10 ** (18 - self.borrowed_token.decimals()) + self.preexisting_supply = ( + self.borrowed_token.totalSupply() + - self.borrowed_token.balanceOf(self.controller) + ) for u in self.accounts: with boa.env.prank(u): - self.collateral_token.approve(self.controller, 2**256-1) - self.borrowed_token.approve(self.controller, 2**256-1) + self.collateral_token.approve(self.controller, 2**256 - 1) + self.borrowed_token.approve(self.controller, 2**256 - 1) @contextmanager def health_calculator(self, user, d_collateral, d_amount): @@ -41,8 +50,12 @@ def health_calculator(self, user, d_collateral, d_amount): calculation_success = True debt = self.controller.debt(user) try: - future_health = self.controller.health_calculator(user, d_collateral, d_amount, False) - future_health_full = self.controller.health_calculator(user, d_collateral, d_amount, True) + future_health = self.controller.health_calculator( + user, d_collateral, d_amount, False + ) + future_health_full = self.controller.health_calculator( + user, d_collateral, d_amount, True + ) except Exception: calculation_success = False @@ -51,8 +64,12 @@ def health_calculator(self, user, d_collateral, d_amount): # If we are here - no exception has happened in the wrapped function assert calculation_success - assert approx(self.controller.health(user), future_health, 1e-4, 1e18 / debt) - assert approx(self.controller.health(user, True), future_health_full, 1e-4, 1e18 / debt) + assert self.controller.health(user) == pytest.approx( + future_health, rel=1e-4, abs=1e18 / debt + ) + assert self.controller.health(user, True) == pytest.approx( + future_health_full, rel=1e-4, abs=1e18 / debt + ) except AllGood: pass @@ -66,12 +83,15 @@ def health_calculator(self, user, d_collateral, d_amount): @rule(c_amount=c_amount, amount_frac=amount_frac, n=n, user_id=user_id) def create_loan(self, c_amount, amount_frac, n, user_id): user = self.accounts[user_id] - amount = min(int(amount_frac * c_amount * 3000), self.debt_ceiling) // self.borrowed_mul + amount = ( + min(int(amount_frac * c_amount * 3000), self.debt_ceiling) + // self.borrowed_mul + ) c_amount = c_amount // self.collateral_mul with boa.env.prank(user): if self.controller.loan_exists(user): - with boa.reverts('Loan already created'): + with boa.reverts("Loan already created"): self.controller.create_loan(c_amount, amount, n) return @@ -79,16 +99,17 @@ def create_loan(self, c_amount, amount_frac, n, user_id): try: self.controller.calculate_debt_n1(c_amount, amount, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.create_loan(c_amount, amount, n) return if self.controller.total_debt() + amount > self.debt_ceiling: if ( - (self.controller.total_debt() + amount) * self.amm.get_rate_mul() > 2**256 - 1 - or c_amount * self.amm.get_p() > 2**256 - 1 + (self.controller.total_debt() + amount) * self.amm.get_rate_mul() + > 2** 256 - 1 + or c_amount * self.amm.get_p() > 2** 256 - 1 ): with boa.reverts(): self.controller.create_loan(c_amount, amount, n) @@ -98,13 +119,13 @@ def create_loan(self, c_amount, amount_frac, n, user_id): return if amount == 0: - with boa.reverts('No loan'): + with boa.reverts("No loan"): self.controller.create_loan(c_amount, amount, n) # It's actually division by zero which happens return try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) except Exception: return # Probably overflow @@ -122,7 +143,11 @@ def create_loan(self, c_amount, amount_frac, n, user_id): try: self.controller.create_loan(c_amount, amount, n) except Exception: - if c_amount * self.collateral_mul // n > 2 * DEAD_SHARES and c_amount * self.collateral_mul // n < (2**128 - 1) // DEAD_SHARES: + if ( + c_amount * self.collateral_mul // n > 2 * DEAD_SHARES + and c_amount * self.collateral_mul // n + < (2**128 - 1) // DEAD_SHARES + ): pass @rule(amount_frac=amount_frac, user_id=user_id) @@ -134,7 +159,6 @@ def repay(self, amount_frac, user_id): with self.health_calculator(user, 0, -amount): with boa.env.prank(user): if amount == 0: - self.controller.repay(amount, user) return if not self.controller.loan_exists(user): with boa.reverts("Loan doesn't exist"): @@ -156,7 +180,7 @@ def add_collateral(self, c_amount, user_id): return try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) except Exception: raise AllGood() @@ -165,7 +189,9 @@ def add_collateral(self, c_amount, user_id): self.controller.add_collateral(c_amount, user) return - if (c_amount + self.amm.get_sum_xy(user)[1]) * self.collateral_mul * self.amm.get_p() > 2**256 - 1: + if ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.collateral_mul * self.amm.get_p() > 2**256 - 1: with boa.reverts(): self.controller.add_collateral(c_amount, user) raise AllGood() @@ -174,19 +200,24 @@ def add_collateral(self, c_amount, user_id): self.controller.add_collateral(c_amount, user) except Exception: # Tick overflow = ok - assert (c_amount + self.amm.get_sum_xy(user)[1]) * self.collateral_mul > (2**128 - 1) // (50 * DEAD_SHARES) + assert ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.collateral_mul > (2**128 - 1) // (50 * DEAD_SHARES) raise AllGood() @rule(c_amount=c_amount, amount_frac=amount_frac, user_id=user_id) def borrow_more(self, c_amount, amount_frac, user_id): user = self.accounts[user_id] - amount = min(int(amount_frac * c_amount * 3000), self.debt_ceiling) // self.borrowed_mul + amount = ( + min(int(amount_frac * c_amount * 3000), self.debt_ceiling) + // self.borrowed_mul + ) c_amount = c_amount // self.collateral_mul with self.health_calculator(user, c_amount, amount): with boa.env.prank(user): try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) except Exception: raise AllGood() @@ -210,14 +241,16 @@ def borrow_more(self, c_amount, amount_frac, user_id): try: self.controller.calculate_debt_n1(final_collateral, final_debt, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.borrow_more(c_amount, amount) raise AllGood() if self.controller.total_debt() + amount > self.debt_ceiling: - if (self.controller.total_debt() + amount) * self.amm.get_rate_mul() * self.collateral_mul > 2**256 - 1: + if ( + self.controller.total_debt() + amount + ) * self.amm.get_rate_mul() * self.collateral_mul > 2**256 - 1: with boa.reverts(): self.controller.borrow_more(c_amount, amount) else: @@ -225,7 +258,10 @@ def borrow_more(self, c_amount, amount_frac, user_id): self.controller.borrow_more(c_amount, amount) raise AllGood() - if final_collateral * self.amm.get_p() * self.collateral_mul > 2**256 - 1: + if ( + final_collateral * self.amm.get_p() * self.collateral_mul + > 2**256 - 1 + ): with boa.reverts(): self.controller.borrow_more(c_amount, amount) raise AllGood() @@ -234,16 +270,26 @@ def borrow_more(self, c_amount, amount_frac, user_id): self.controller.borrow_more(c_amount, amount) except Exception: # Tick overflow = ok - assert (c_amount + self.amm.get_sum_xy(user)[1]) * self.collateral_mul > (2**128 - 1) // (50 * DEAD_SHARES) + assert ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.collateral_mul > (2**128 - 1) // (50 * DEAD_SHARES) raise AllGood() @invariant() def debt_supply(self): - assert self.controller.total_debt() == self.borrowed_token.totalSupply() - self.preexisting_supply - self.borrowed_token.balanceOf(self.controller) + assert ( + self.controller.total_debt() + == self.borrowed_token.totalSupply() + - self.preexisting_supply + - self.borrowed_token.balanceOf(self.controller) + ) @invariant() def sum_of_debts(self): - assert sum(self.controller.debt(u) for u in self.accounts) == self.controller.total_debt() + assert ( + sum(self.controller.debt(u) for u in self.accounts) + == self.controller.total_debt() + ) @invariant() def health(self): @@ -252,14 +298,18 @@ def health(self): assert self.controller.health(user) > 0 -def test_stateful_lendborrow(market_amm, filled_controller, collateral_token, borrowed_token, accounts): - StatefulLendBorrow.TestCase.settings = settings(max_examples=200, stateful_step_count=20) +def test_stateful_lendborrow( + amm, controller, collateral_token, borrowed_token, accounts +): + StatefulLendBorrow.TestCase.settings = settings( + max_examples=200, stateful_step_count=20 + ) for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) run_state_machine_as_test(StatefulLendBorrow) -def test_health_mismatch(market_amm, filled_controller, collateral_token, borrowed_token, accounts): +def test_health_mismatch(amm, controller, collateral_token, borrowed_token, accounts): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() @@ -267,7 +317,7 @@ def test_health_mismatch(market_amm, filled_controller, collateral_token, borrow state.health() state.sum_of_debts() state.create_loan(amount_frac=1.0, c_amount=10000000000000, n=5, user_id=0) - debt = filled_controller.debt(accounts[0]) + debt = controller.debt(accounts[0]) if debt == 0: return state.debt_supply() diff --git a/tests/lending/test_health_in_trades.py b/tests/lending/test_health_in_trades.py index 0386797e..9d4b3420 100644 --- a/tests/lending/test_health_in_trades.py +++ b/tests/lending/test_health_in_trades.py @@ -2,7 +2,13 @@ from math import ceil from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant, initialize +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, + initialize, +) class AdiabaticTrader(RuleBasedStateMachine): @@ -12,18 +18,23 @@ class AdiabaticTrader(RuleBasedStateMachine): is_pump = st.booleans() n = st.integers(min_value=5, max_value=50) t = st.integers(min_value=0, max_value=86400) - rate = st.integers(min_value=int(1e18 * 0.001 / 365 / 86400), max_value=int(1e18 * 0.2 / 365 / 86400)) + rate = st.integers( + min_value=int(1e18 * 0.001 / 365 / 86400), + max_value=int(1e18 * 0.2 / 365 / 86400), + ) not_enough_allowed = True def __init__(self): super().__init__() self.collateral = self.collateral_token - self.amm = self.market_amm - self.controller = self.filled_controller - self.borrowed_mul = 10**(18 - self.borrowed_token.decimals()) - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) + self.amm = self.amm + self.controller = self.controller + self.borrowed_mul = 10 ** (18 - self.borrowed_token.decimals()) + self.collateral_mul = 10 ** (18 - self.collateral_token.decimals()) with boa.env.prank(self.admin): - self.market_mpolicy.set_rates(int(1e18 * 0.04 / 365 / 86400), int(1e18 * 0.04 / 365 / 86400)) + self.monetary_policy.set_rates( + int(1e18 * 0.04 / 365 / 86400), int(1e18 * 0.04 / 365 / 86400) + ) for user in self.accounts[:2]: with boa.env.prank(user): self.borrowed_token.approve(self.controller, 2**256 - 1) @@ -34,12 +45,16 @@ def __init__(self): @initialize(collateral_amount=collateral_amount, n=n) def initializer(self, collateral_amount, n): # Calculating so that we create it with a nonzero loan - collateral_amount = int(max(collateral_amount / self.collateral_mul, - n * 10 * ceil(3000 * max(self.borrowed_mul / self.collateral_mul, 1)), - n)) + collateral_amount = int( + max( + collateral_amount / self.collateral_mul, + n * 10 * ceil(3000 * max(self.borrowed_mul / self.collateral_mul, 1)), + n, + ) + ) user = self.accounts[0] with boa.env.prank(user): - self.collateral._mint_for_testing(user, collateral_amount) + boa.deal(self.collateral, user, collateral_amount) loan_amount = self.controller.max_borrowable(collateral_amount, n) self.controller.create_loan(collateral_amount, loan_amount, n) self.borrowed_token.transfer(self.accounts[1], loan_amount) @@ -59,7 +74,7 @@ def trade_to_price(self, p): return self.amm.exchange(0, 1, amount, 0) else: - self.collateral._mint_for_testing(user, amount) + boa.deal(self.collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(oracle_step=oracle_step) @@ -75,11 +90,14 @@ def random_trade(self, amount_fraction, is_pump): user = self.accounts[0] with boa.env.prank(user): if is_pump: - amount = min(int(self.loan_amount * amount_fraction), self.borrowed_token.balanceOf(user)) + amount = min( + int(self.loan_amount * amount_fraction), + self.borrowed_token.balanceOf(user), + ) self.amm.exchange(0, 1, amount, 0) else: amount = int(self.collateral_amount * amount_fraction) - self.collateral._mint_for_testing(user, amount) + boa.deal(self.collateral, user, amount) self.amm.exchange(1, 0, amount, 0) @rule(t=t) @@ -89,7 +107,9 @@ def time_travel(self, t): @rule(min_rate=rate, max_rate=rate) def change_rate(self, min_rate, max_rate): with boa.env.prank(self.admin): - self.market_mpolicy.set_rates(min(min_rate, max_rate), max(min_rate, max_rate)) + self.monetary_policy.set_rates( + min(min_rate, max_rate), max(min_rate, max_rate) + ) @invariant() def health(self): @@ -98,8 +118,19 @@ def health(self): assert h > 0 -def test_adiabatic_follow(market_amm, filled_controller, market_mpolicy, collateral_token, borrowed_token, price_oracle, accounts, admin): - AdiabaticTrader.TestCase.settings = settings(max_examples=50, stateful_step_count=50) +def test_adiabatic_follow( + amm, + controller, + monetary_policy, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, +): + AdiabaticTrader.TestCase.settings = settings( + max_examples=50, stateful_step_count=50 + ) for k, v in locals().items(): setattr(AdiabaticTrader, k, v) run_state_machine_as_test(AdiabaticTrader) diff --git a/tests/lending/test_max_borrowable.py b/tests/lending/test_max_borrowable.py index c5621438..8ef8068e 100644 --- a/tests/lending/test_max_borrowable.py +++ b/tests/lending/test_max_borrowable.py @@ -1,70 +1,21 @@ -import boa -from hypothesis import given, settings -from hypothesis import strategies as st -from ..conftest import approx - - -DEAD_SHARES = 1000 - - -@given( - collateral_amount=st.integers(min_value=100, max_value=10**20), - n=st.integers(min_value=4, max_value=50), - f_p_o=st.floats(min_value=0.7, max_value=1.3)) -@settings(max_examples=1000) -def test_max_borrowable(borrowed_token, collateral_token, market_amm, filled_controller, price_oracle, admin, - collateral_amount, n, f_p_o): - # Create some liquidity and go into the band - with boa.env.prank(admin): - c_amount = 10**collateral_token.decimals() - collateral_token._mint_for_testing(admin, c_amount) - collateral_token.approve(filled_controller, 2**256-1) - filled_controller.create_loan(c_amount, filled_controller.max_borrowable(c_amount, 5), 5) - borrowed_token.approve(market_amm.address, 2**256-1) - market_amm.exchange(0, 1, 100, 0) - - # Change oracle - p_o = int(price_oracle.price() * f_p_o) - with boa.env.prank(admin): - price_oracle.set_price(p_o) - - max_borrowable = filled_controller.max_borrowable(collateral_amount, n) - total = borrowed_token.balanceOf(filled_controller.address) - if max_borrowable >= total: - return - with boa.reverts(): - filled_controller.calculate_debt_n1(collateral_amount, int(max_borrowable * 1.001) + n, n) - if max_borrowable == 0: - return - filled_controller.calculate_debt_n1(collateral_amount, max_borrowable, n) - - min_collateral = filled_controller.min_collateral(max_borrowable, n) - assert approx(min_collateral, - collateral_amount, 1e-6 + (n**2 + n * DEAD_SHARES) * ( - 1 / min(min_collateral, collateral_amount) + 1 / max_borrowable)) - - -@given( - debt_amount=st.integers(min_value=100, max_value=10**20), - n=st.integers(min_value=4, max_value=50), - f_p_o=st.floats(min_value=0.7, max_value=1.3)) -@settings(max_examples=1000) -def test_min_collateral(borrowed_token, collateral_token, market_amm, filled_controller, price_oracle, admin, - debt_amount, n, f_p_o): - # Create some liquidity and go into the band - with boa.env.prank(admin): - c_amount = 10**collateral_token.decimals() - collateral_token._mint_for_testing(admin, c_amount) - collateral_token.approve(filled_controller, 2**256-1) - filled_controller.create_loan(c_amount, filled_controller.max_borrowable(c_amount, 5), 5) - borrowed_token.approve(market_amm.address, 2**256-1) - market_amm.exchange(0, 1, 10**2, 0) - - # Change oracle - p_o = int(price_oracle.price() * f_p_o) - with boa.env.prank(admin): - price_oracle.set_price(p_o) - - min_collateral = filled_controller.min_collateral(debt_amount, n) - - filled_controller.calculate_debt_n1(min_collateral, debt_amount, n) +from tests.utils.constants import MAX_UINT256 + + +def test_cap_zero_makes_max_borrowable_zero(controller, admin): + c_amount = 10**18 + n = 5 + baseline = controller.max_borrowable(c_amount, n) + assert baseline >= 0 # sanity + controller.set_borrow_cap(0, sender=admin) + assert controller.max_borrowable(c_amount, n) == 0 + + +def test_default_behavior_positive_headroom(controller, admin): + c_amount = 10**24 + n = 5 + controller.set_borrow_cap(0, sender=admin) + assert controller.max_borrowable(c_amount, n) == 0 + borrowed_balance = controller.available_balance() + controller.set_borrow_cap(MAX_UINT256, sender=admin) + # With large collateral, max_borrowable equals the available borrowed balance cap precisely + assert controller.max_borrowable(c_amount, n) == borrowed_balance diff --git a/tests/lending/test_monetary_policy.py b/tests/lending/test_monetary_policy.py index 3ab75317..140601d9 100644 --- a/tests/lending/test_monetary_policy.py +++ b/tests/lending/test_monetary_policy.py @@ -1,30 +1,38 @@ import boa +import pytest from hypothesis import given from hypothesis import strategies as st -from ..conftest import approx - -min_default_borrow_rate = 5 * 10**15 // (365 * 86400) -max_default_borrow_rate = 50 * 10**16 // (365 * 86400) @given(fill=st.floats(min_value=0.0, max_value=2.0)) -def test_monetary_policy(filled_controller, collateral_token, borrowed_token, market_mpolicy, admin, fill): - available = borrowed_token.balanceOf(filled_controller) +def test_monetary_policy( + controller, collateral_token, borrowed_token, monetary_policy, admin, fill +): + available = borrowed_token.balanceOf(controller.address) to_borrow = int(fill * available) - c_amount = 2 * 3000 * to_borrow * 10**(18 - borrowed_token.decimals()) // 10**(18 - collateral_token.decimals()) + c_amount = ( + 2 + * 3000 + * to_borrow + * 10 ** (18 - borrowed_token.decimals()) + // 10 ** (18 - collateral_token.decimals()) + ) if to_borrow > 0 and c_amount > 0: with boa.env.prank(admin): - collateral_token.approve(filled_controller, 2**256 - 1) - collateral_token._mint_for_testing(admin, c_amount) + collateral_token.approve(controller.address, 2**256 - 1) + boa.deal(collateral_token, admin, c_amount) if to_borrow > available: with boa.reverts(): - filled_controller.create_loan(c_amount, to_borrow, 5) + controller.create_loan(c_amount, to_borrow, 5) return else: - filled_controller.create_loan(c_amount, to_borrow, 5) - rate = market_mpolicy.rate(filled_controller) - assert rate >= min_default_borrow_rate * (1 - 1e-5) - assert rate <= max_default_borrow_rate * (1 + 1e-5) - theoretical_rate = min_default_borrow_rate * (max_default_borrow_rate / min_default_borrow_rate)**fill - assert approx(rate, theoretical_rate, 1e-4) + controller.create_loan(c_amount, to_borrow, 5) + # Use the policy’s configured min/max rates bound to this market + min_rate = monetary_policy.min_rate() + max_rate = monetary_policy.max_rate() + rate = monetary_policy.rate(controller.address) + assert rate >= min_rate * (1 - 1e-5) + assert rate <= max_rate * (1 + 1e-5) + theoretical_rate = min_rate * (max_rate / min_rate) ** fill + assert rate == pytest.approx(theoretical_rate, rel=1e-4) diff --git a/tests/lending/test_oracle_attack.py b/tests/lending/test_oracle_attack.py index 112a6bc8..67bca32c 100644 --- a/tests/lending/test_oracle_attack.py +++ b/tests/lending/test_oracle_attack.py @@ -12,170 +12,179 @@ from hypothesis import given, settings from hypothesis import strategies as st +from tests.utils.constants import MAX_UINT256 -MAX = 2**256 - 1 - - -@pytest.fixture(scope='module') -def collateral_token(get_collateral_token): - decimals = 18 - return get_collateral_token(decimals) - +@pytest.fixture(scope="module") +def market_type(): + # Ensure we test against the lending market setup from tests/conftest.py + return "lending" -@pytest.fixture(scope='module') -def borrowed_token(stablecoin): - return stablecoin +@pytest.fixture(scope="module") +def borrow_cap(): + # Use the dedicated fixture to set an effectively-unbounded cap for this test module + return MAX_UINT256 -@pytest.fixture(scope='module') -def victim(accounts): - return accounts[1] +@given( + victim_gap=st.floats(min_value=15 / 866, max_value=0.9), + victim_bins=st.integers(min_value=4, max_value=50), +) +@settings(max_examples=10000) +@pytest.mark.xfail(strict=True) +def test_vuln( + vault, + controller, + amm, + admin, + borrowed_token, + price_oracle, + collateral_token, + accounts, + victim_gap, + victim_bins, +): + victim = accounts[1] + hacker = accounts[2] -@pytest.fixture(scope='module') -def hacker(accounts): - return accounts[2] + # victim loan + victim_collateral_lent = int(10_000e18) + price_manipulation = ( + 15 / 866 + ) # 866-second price oracle manipulation during 15 second (1 block) + manipulation_time = 13 # time between two blocks + p = price_oracle.price() -@pytest.fixture(scope="module") -def factory_new(factory_partial, stablecoin, amm_impl, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, gauge_impl, admin): + # Configure dynamic fee for this scenario with boa.env.prank(admin): - return factory_partial.deploy( - stablecoin.address, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, gauge_impl, - admin) - + controller.set_amm_fee(int(0.00001 * 1e18)) -@pytest.fixture(scope="module") -def amm_old_interface(): - return boa.load_partial('contracts/testing/OldAMM.vy') + # hacker + hacker_crvusd_reserves = victim_collateral_lent * p // 10**18 + # approve everything + for user in (hacker, victim): + with boa.env.prank(user): + for token in (borrowed_token, collateral_token): + for contract in (controller, amm, vault): + token.approve(contract.address, MAX_UINT256) -@pytest.fixture(scope="module") -def factory_old(factory_partial, stablecoin, controller_impl, vault_impl, price_oracle_impl, mpolicy_impl, gauge_impl, amm_old_interface, admin): + # add crvUSD to the vault with boa.env.prank(admin): - amm_impl = amm_old_interface.deploy_as_blueprint() - return factory_partial.deploy( - stablecoin.address, - amm_impl, controller_impl, vault_impl, - price_oracle_impl, mpolicy_impl, gauge_impl, - admin) - + b_amount = victim_collateral_lent * p // 10**18 + boa.deal(borrowed_token, admin, b_amount) + borrowed_token.approve(vault.address, MAX_UINT256) + vault.deposit(b_amount) -@pytest.fixture(scope='module') -def vault_new(factory_new, vault_impl, borrowed_token, collateral_token, price_oracle, admin): - with boa.env.prank(admin): - price_oracle.set_price(int(1e18)) - - vault = vault_impl.at( - factory_new.create( - borrowed_token.address, collateral_token.address, - 100, int(0.002 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), - price_oracle.address, "Test" - ) + # victim creates a loan + with boa.env.prank(victim): + victim_borrow = int( + (1 - victim_gap) + * controller.max_borrowable(victim_collateral_lent, victim_bins) ) + boa.deal(collateral_token, victim, victim_collateral_lent) + controller.create_loan(victim_collateral_lent, victim_borrow, victim_bins) + initial_health = controller.health(victim, True) / 1e18 - boa.env.time_travel(120) - - return vault - + # hacker manipulates price oracle + with boa.env.prank(hacker): + boa.deal(borrowed_token, hacker, hacker_crvusd_reserves) + amm.exchange(0, 1, hacker_crvusd_reserves, 0) -@pytest.fixture(scope='module') -def vault_old(factory_old, vault_impl, borrowed_token, collateral_token, price_oracle, admin): + # update oracle price with boa.env.prank(admin): - price_oracle.set_price(int(1e18)) - - vault = vault_impl.at( - factory_old.create( - borrowed_token.address, collateral_token.address, - 100, int(0.006 * 1e18), int(0.09 * 1e18), int(0.06 * 1e18), - price_oracle.address, "Test" - ) - ) - - boa.env.time_travel(120) - - return vault - - -@pytest.fixture(scope='module') -def controller_new(vault_new, controller_interface): - return controller_interface.at(vault_new.controller()) - - -@pytest.fixture(scope='module') -def amm_new(vault_new, amm_interface): - return amm_interface.at(vault_new.amm()) - + new_p = int(p * (1 + price_manipulation)) + price_oracle.set_price(new_p) -@pytest.fixture(scope='module') -def controller_old(vault_old, controller_interface): - return controller_interface.at(vault_old.controller()) + boa.env.time_travel(manipulation_time) + with boa.env.prank(hacker): + victim_health = controller.health(victim, True) / 1e18 + with boa.reverts(): + controller.liquidate(victim, 0) -@pytest.fixture(scope='module') -def amm_old(vault_old, amm_old_interface): - return amm_old_interface.at(vault_old.amm()) + # If liquidation succeeded + crvusd_profit = borrowed_token.balanceOf(hacker) - hacker_crvusd_reserves + print("crvusd profit", crvusd_profit / 1e18) + collateral_profit = collateral_token.balanceOf(hacker) + print("Collateral profit", collateral_profit / 1e18) + profit = crvusd_profit + collateral_profit * (p / 1e18) + print("Total profit", profit / 1e18) + print(f"Health: {initial_health} -> {victim_health}") -def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins, fee): +@pytest.mark.xfail(strict=True) +def test_vuln_lite( + vault, + controller, + amm, + admin, + borrowed_token, + price_oracle, + collateral_token, + accounts, +): + victim_gap = 0 + victim_bins = 4 + victim = accounts[1] + hacker = accounts[2] # victim loan victim_collateral_lent = int(10_000_000e18) - price_manipulation = 15 / 866 # 866-second price oracle manipulation during 15 second (1 block) + price_manipulation = ( + 15 / 866 + ) # 866-second price oracle manipulation during 15 second (1 block) manipulation_time = 13 # time between two blocks p = price_oracle.price() + # Configure dynamic fee for this scenario with boa.env.prank(admin): - controller.set_amm_fee(int(fee * 1e18)) + controller.set_amm_fee(int(0.00001 * 1e18)) # hacker - hacker_crvusd_reserves = int(500_000_000e18) + hacker_crvusd_reserves = victim_collateral_lent * p // 10**18 # approve everything - for user in hacker, victim: + for user in (hacker, victim): with boa.env.prank(user): - for token in borrowed_token, collateral_token: - for contract in controller, amm, vault: - token.approve(contract.address, MAX) + for token in (borrowed_token, collateral_token): + for contract in (controller, amm, vault): + token.approve(contract.address, MAX_UINT256) # add crvUSD to the vault with boa.env.prank(admin): - b_amount = int(1_000_000_000e18) - borrowed_token._mint_for_testing(admin, b_amount) - borrowed_token.approve(vault.address, MAX) + b_amount = victim_collateral_lent * p // 10**18 + boa.deal(borrowed_token, admin, b_amount) + borrowed_token.approve(vault.address, MAX_UINT256) vault.deposit(b_amount) # victim creates a loan with boa.env.prank(victim): - victim_borrow = int((1 - victim_gap) * controller.max_borrowable(victim_collateral_lent, victim_bins)) - collateral_token._mint_for_testing(victim, victim_collateral_lent) + victim_borrow = int( + (1 - victim_gap) + * controller.max_borrowable(victim_collateral_lent, victim_bins) + ) + boa.deal(collateral_token, victim, victim_collateral_lent) controller.create_loan(victim_collateral_lent, victim_borrow, victim_bins) initial_health = controller.health(victim, True) / 1e18 - # print("Victim health", initial_health) - # hacker manipulates price oracle and liquidates the victim + # hacker manipulates price oracle with boa.env.prank(hacker): - borrowed_token._mint_for_testing(hacker, hacker_crvusd_reserves) - spent, received = amm.exchange(0, 1, hacker_crvusd_reserves, 0) - # print(f"Bought {received/1e18:.3f} for {spent/1e18:.2f}") + boa.deal(borrowed_token, hacker, hacker_crvusd_reserves) + amm.exchange(0, 1, hacker_crvusd_reserves, 0) # update oracle price with boa.env.prank(admin): new_p = int(p * (1 + price_manipulation)) price_oracle.set_price(new_p) - # print(f"Manipulated collateral price {new_p/1e18:.4f}") boa.env.time_travel(manipulation_time) with boa.env.prank(hacker): victim_health = controller.health(victim, True) / 1e18 - # print("Victim health", victim_health) - with boa.reverts(): controller.liquidate(victim, 0) @@ -187,27 +196,3 @@ def template_vuln_test(vault, controller, amm, admin, borrowed_token, price_orac profit = crvusd_profit + collateral_profit * (p / 1e18) print("Total profit", profit / 1e18) print(f"Health: {initial_health} -> {victim_health}") - - -@given( - victim_gap=st.floats(min_value=15 / 866, max_value=0.9), - victim_bins=st.integers(min_value=4, max_value=50) -) -@settings(max_examples=10000) -def test_vuln_new(vault_new, controller_new, amm_new, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins): - - template_vuln_test(vault_new, controller_new, amm_new, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins, fee=0.00001) # Any fee is safe - even very low - - -@given( - victim_gap=st.floats(min_value=15 / 866, max_value=0.9), - victim_bins=st.integers(min_value=4, max_value=50) -) -@settings(max_examples=10000) -def test_vuln_old(vault_old, controller_old, amm_old, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins): - - template_vuln_test(vault_old, controller_old, amm_old, admin, borrowed_token, price_oracle, collateral_token, victim, hacker, - victim_gap, victim_bins, fee=0.019) # 1.9% fee or higher is safe diff --git a/tests/lending/test_pool_price_oracle.py b/tests/lending/test_pool_price_oracle.py index 9addc500..087b3d31 100644 --- a/tests/lending/test_pool_price_oracle.py +++ b/tests/lending/test_pool_price_oracle.py @@ -1,18 +1,23 @@ import boa import pytest from itertools import permutations +from tests.utils.deployers import ( + MOCK_SWAP2_DEPLOYER, + MOCK_SWAP3_DEPLOYER, + CRYPTO_FROM_POOL_DEPLOYER, +) @pytest.fixture() def swap2(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/MockSwap2.vy') + return MOCK_SWAP2_DEPLOYER.deploy() @pytest.fixture() def swap3(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/MockSwap3.vy') + return MOCK_SWAP3_DEPLOYER.deploy() @pytest.mark.parametrize("coin_ids", [[0, 1], [1, 0]] + list(permutations([0, 1, 2]))) @@ -23,7 +28,7 @@ def test_oracle(swap2, swap3, coin_ids): else: swap = swap3 borrowed_ix, collateral_ix = coin_ids[:2] - oracle = boa.load('contracts/price_oracles/CryptoFromPool.vy', swap, N, borrowed_ix, collateral_ix) + oracle = CRYPTO_FROM_POOL_DEPLOYER.deploy(swap, N, borrowed_ix, collateral_ix) p0 = 0.1 for p in [1, 10, 100]: diff --git a/tests/lending/test_secondary_mpolicy.py b/tests/lending/test_secondary_mpolicy.py index 3067b989..5f27fab5 100644 --- a/tests/lending/test_secondary_mpolicy.py +++ b/tests/lending/test_secondary_mpolicy.py @@ -3,6 +3,12 @@ from hypothesis import given from hypothesis import settings from hypothesis import strategies as st +from tests.utils.deployers import ( + MOCK_FACTORY_DEPLOYER, + MOCK_MARKET_DEPLOYER, + MOCK_RATE_SETTER_DEPLOYER, + SECONDARY_MONETARY_POLICY_DEPLOYER, +) MIN_UTIL = 10**16 @@ -15,28 +21,30 @@ @pytest.fixture(scope="module") def factory(): - return boa.load('contracts/testing/MockFactory.vy') + return MOCK_FACTORY_DEPLOYER.deploy() @pytest.fixture(scope="module") def controller(): - return boa.load('contracts/testing/MockMarket.vy') + return MOCK_MARKET_DEPLOYER.deploy() @pytest.fixture(scope="module") def amm(): - return boa.load('contracts/testing/MockRateSetter.vy', RATE0) - - -@pytest.fixture(scope="module") -def borrowed_token(get_borrowed_token): - return get_borrowed_token(18) + return MOCK_RATE_SETTER_DEPLOYER.deploy(RATE0) @pytest.fixture(scope="module") def mp(factory, amm, borrowed_token): - return boa.load('contracts/mpolicies/SecondaryMonetaryPolicy.vy', factory, amm, borrowed_token, - int(0.85 * 1e18), int(0.5 * 1e18), int(3 * 1e18), 0) + return SECONDARY_MONETARY_POLICY_DEPLOYER.deploy( + factory, + amm, + borrowed_token, + int(0.85 * 1e18), + int(0.5 * 1e18), + int(3 * 1e18), + 0, + ) @given( @@ -45,14 +53,31 @@ def mp(factory, amm, borrowed_token): u_0=st.integers(0, 10**18), min_ratio=st.integers(10**15, 10**19), max_ratio=st.integers(10**15, 10**19), - shift=st.integers(0, 101 * 10**18) + shift=st.integers(0, 101 * 10**18), ) @settings(max_examples=10000) -def test_mp(mp, factory, controller, borrowed_token, amm, total_debt, balance, u_0, min_ratio, max_ratio, shift): - if u_0 >= int(0.2e18) and u_0 <= int(0.98e18) and \ - min_ratio > int(1e17) and max_ratio < (10e17) and \ - min_ratio < int(0.9e18) and max_ratio > int(1.1e18) and\ - shift <= 100 * 10**18: +def test_mp( + mp, + factory, + controller, + borrowed_token, + amm, + total_debt, + balance, + u_0, + min_ratio, + max_ratio, + shift, +): + if ( + u_0 >= int(0.2e18) + and u_0 <= int(0.98e18) + and min_ratio > int(1e17) + and max_ratio < (10e17) + and min_ratio < int(0.9e18) + and max_ratio > int(1.1e18) + and shift <= 100 * 10**18 + ): # These parameters will certainly work mp.set_parameters(u_0, min_ratio, max_ratio, shift) else: @@ -66,7 +91,7 @@ def test_mp(mp, factory, controller, borrowed_token, amm, total_debt, balance, u assert max_ratio <= MAX_HIGH_RATIO and max_ratio >= 10**18 controller.set_debt(total_debt) - borrowed_token._mint_for_testing(controller.address, balance) + boa.deal(borrowed_token, controller.address, balance) rate = mp.rate(controller.address) diff --git a/tests/lending/test_shifted_trades.py b/tests/lending/test_shifted_trades.py index 15f6e1e2..01a9e264 100644 --- a/tests/lending/test_shifted_trades.py +++ b/tests/lending/test_shifted_trades.py @@ -21,7 +21,16 @@ def trade_to_price(self, p): super().trade_to_price(int(p * (1 + self.price_shift))) -def test_adiabatic_shifted(market_amm, filled_controller, market_mpolicy, collateral_token, borrowed_token, price_oracle, accounts, admin): +def test_adiabatic_shifted( + amm, + controller, + monetary_policy, + collateral_token, + borrowed_token, + price_oracle, + accounts, + admin, +): ShiftedTrader.TestCase.settings = settings(max_examples=50, stateful_step_count=50) for k, v in locals().items(): setattr(ShiftedTrader, k, v) diff --git a/tests/lending/test_st_interest_conservation.py b/tests/lending/test_st_interest_conservation.py index 51253805..e27c2f16 100644 --- a/tests/lending/test_st_interest_conservation.py +++ b/tests/lending/test_st_interest_conservation.py @@ -1,18 +1,24 @@ import boa from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) -DEAD_SHARES = 1000 -MIN_RATE = 10**15 // (365 * 86400) # 0.1% -MAX_RATE = 10**19 // (365 * 86400) # 1000% +from tests.utils.constants import DEAD_SHARES + +MIN_RATE = 10**15 // (365 * 86400) # 0.1% +MAX_RATE = 10**19 // (365 * 86400) # 1000% class StatefulLendBorrow(RuleBasedStateMachine): n = st.integers(min_value=5, max_value=50) - amount = st.integers(min_value=0, max_value=2**256-1) - c_amount = st.integers(min_value=0, max_value=2**256-1) + amount = st.integers(min_value=0, max_value=2**256 - 1) + c_amount = st.integers(min_value=0, max_value=2**256 - 1) user_id = st.integers(min_value=0, max_value=9) t = st.integers(min_value=0, max_value=86400 * 365) min_rate = st.integers(min_value=0, max_value=2**255 - 1) @@ -21,18 +27,19 @@ class StatefulLendBorrow(RuleBasedStateMachine): def __init__(self): super().__init__() self.collateral = self.collateral_token - self.amm = self.market_amm - self.controller = self.market_controller - self.borrowed_precision = 10**(18 - self.borrowed_token.decimals()) - self.collateral_precision = 10**(18 - self.collateral_token.decimals()) + # Use standard fixture names from tests/conftest.py + self.amm = self.amm + self.controller = self.controller + self.borrowed_precision = 10 ** (18 - self.borrowed_token.decimals()) + self.collateral_precision = 10 ** (18 - self.collateral_token.decimals()) for u in self.accounts: with boa.env.prank(u): - self.collateral_token.approve(self.controller.address, 2**256-1) - self.borrowed_token.approve(self.controller.address, 2**256-1) - self.debt_ceiling = 10**6 * 10**(self.borrowed_token.decimals()) + self.collateral_token.approve(self.controller.address, 2**256 - 1) + self.borrowed_token.approve(self.controller.address, 2**256 - 1) + self.debt_ceiling = 10**6 * 10 ** (self.borrowed_token.decimals()) with boa.env.prank(self.accounts[0]): self.borrowed_token.approve(self.vault.address, 2**256 - 1) - self.borrowed_token._mint_for_testing(self.accounts[0], self.debt_ceiling) + boa.deal(self.borrowed_token, self.accounts[0], self.debt_ceiling) self.vault.deposit(self.debt_ceiling) @rule(c_amount=c_amount, amount=amount, n=n, user_id=user_id) @@ -41,7 +48,7 @@ def create_loan(self, c_amount, amount, n, user_id): with boa.env.prank(user): if self.controller.loan_exists(user): - with boa.reverts('Loan already created'): + with boa.reverts("Loan already created"): self.controller.create_loan(c_amount, amount, n) return @@ -49,9 +56,9 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.calculate_debt_n1(c_amount, amount, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.create_loan(c_amount, amount, n) return @@ -67,7 +74,7 @@ def create_loan(self, c_amount, amount, n, user_id): return try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -81,7 +88,14 @@ def create_loan(self, c_amount, amount, n, user_id): self.controller.create_loan(c_amount, amount, n) return except Exception as e: - if ('Too deep' in str(e) and c_amount * self.collateral_precision * 3000 / (amount * self.borrowed_precision) < 1e-3) or 'Amount too low' in str(e): + if ( + "Too deep" in str(e) + and c_amount + * self.collateral_precision + * 3000 + / (amount * self.borrowed_precision) + < 1e-3 + ) or "Amount too low" in str(e): return else: raise @@ -89,19 +103,29 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.create_loan(c_amount, amount, n) except Exception as e: - if 'Too deep' in str(e) and (c_amount * 3000 * self.collateral_precision) / (amount * self.borrowed_precision) < 1e-3: + if ( + "Too deep" in str(e) + and (c_amount * 3000 * self.collateral_precision) + / (amount * self.borrowed_precision) + < 1e-3 + ): pass else: - if c_amount * self.collateral_precision // n <= (2**128 - 1) // DEAD_SHARES: + if ( + c_amount * self.collateral_precision // n + <= (2**128 - 1) // DEAD_SHARES + ): raise @rule(amount=amount, user_id=user_id) def repay(self, amount, user_id): + if amount == 0: + return user = self.accounts[user_id] to_repay = min(self.controller.debt(user), amount) user_balance = self.borrowed_token.balanceOf(user) if to_repay > user_balance: - self.borrowed_token._mint_for_testing(user, to_repay - user_balance) + boa.deal(self.borrowed_token, user, to_repay - user_balance) with boa.env.prank(user): if amount == 0: @@ -121,7 +145,7 @@ def add_collateral(self, c_amount, user_id): with boa.env.prank(user): try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -134,7 +158,9 @@ def add_collateral(self, c_amount, user_id): self.controller.add_collateral(c_amount, user) return - if (c_amount + self.amm.get_sum_xy(user)[1]) * self.collateral_precision * self.amm.get_p() > 2**256 - 1: + if ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.collateral_precision * self.amm.get_p() > 2**256 - 1: with boa.reverts(): self.controller.add_collateral(c_amount, user) return @@ -142,7 +168,9 @@ def add_collateral(self, c_amount, user_id): try: self.controller.add_collateral(c_amount, user) except Exception: - if (c_amount + self.amm.get_sum_xy(user)[1]) * self.collateral_precision < (2**128 - 1) // DEAD_SHARES: + if ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.collateral_precision < (2**128 - 1) // DEAD_SHARES: raise @rule(c_amount=c_amount, amount=amount, user_id=user_id) @@ -155,7 +183,7 @@ def borrow_more(self, c_amount, amount, user_id): return try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -175,9 +203,9 @@ def borrow_more(self, c_amount, amount, user_id): try: self.controller.calculate_debt_n1(final_collateral, final_debt, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.borrow_more(c_amount, amount) return @@ -186,7 +214,10 @@ def borrow_more(self, c_amount, amount, user_id): self.controller.borrow_more(c_amount, amount) return - if final_collateral * self.collateral_precision // n > (2**128 - 1) // DEAD_SHARES: + if ( + final_collateral * self.collateral_precision // n + > (2**128 - 1) // DEAD_SHARES + ): with boa.reverts(): self.controller.borrow_more(c_amount, amount) return @@ -200,11 +231,17 @@ def time_travel(self, t): @rule(min_rate=min_rate, max_rate=max_rate) def change_rate(self, min_rate, max_rate): with boa.env.prank(self.admin): - if (min_rate > max_rate or min_rate < MIN_RATE or max_rate < MIN_RATE or min_rate > MAX_RATE or max_rate > MAX_RATE): + if ( + min_rate > max_rate + or min_rate < MIN_RATE + or max_rate < MIN_RATE + or min_rate > MAX_RATE + or max_rate > MAX_RATE + ): with boa.reverts(): - self.market_mpolicy.set_rates(min_rate, max_rate) + self.monetary_policy.set_rates(min_rate, max_rate) else: - self.market_mpolicy.set_rates(min_rate, max_rate) + self.monetary_policy.set_rates(min_rate, max_rate) @invariant() def sum_of_debts(self): @@ -220,17 +257,39 @@ def debt_payable(self): supply = self.borrowed_token.totalSupply() b = self.borrowed_token.balanceOf(self.controller) debt = self.controller.total_debt() - assert debt + 10 >= supply - b # Can have error of 1 (rounding) at most per step (and 10 stateful steps) - - -def test_stateful_lendborrow(vault, market_amm, market_controller, market_mpolicy, collateral_token, borrowed_token, accounts, admin): - StatefulLendBorrow.TestCase.settings = settings(max_examples=200, stateful_step_count=10) + assert ( + debt + 10 >= supply - b + ) # Can have error of 1 (rounding) at most per step (and 10 stateful steps) + + +def test_stateful_lendborrow( + vault, + amm, + controller, + monetary_policy, + collateral_token, + borrowed_token, + accounts, + admin, +): + StatefulLendBorrow.TestCase.settings = settings( + max_examples=200, stateful_step_count=10 + ) for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) run_state_machine_as_test(StatefulLendBorrow) -def test_borrow_not_reverting(vault, market_amm, market_controller, market_mpolicy, collateral_token, borrowed_token, accounts, admin): +def test_borrow_not_reverting( + vault, + amm, + controller, + monetary_policy, + collateral_token, + borrowed_token, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() diff --git a/tests/lending/test_twoway_vault.py b/tests/lending/test_twoway_vault.py deleted file mode 100644 index 778c7379..00000000 --- a/tests/lending/test_twoway_vault.py +++ /dev/null @@ -1,90 +0,0 @@ -import boa - - -DEAD_SHARES = 1000 -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" - - -def test_vault_creation(factory_2way, vault_long, vault_short, - controller_long, controller_short, amm_long, amm_short, - collateral_token, borrowed_token, price_oracle, stablecoin): - assert controller_long.borrowed_token() == borrowed_token.address - assert controller_short.borrowed_token() == collateral_token.address - assert controller_long.collateral_token() == vault_short.address - assert controller_short.collateral_token() == vault_long.address - - assert amm_long.price_oracle() == price_oracle.price() // DEAD_SHARES - assert amm_short.price_oracle() == (10**18) ** 2 // DEAD_SHARES // price_oracle.price() - n = factory_2way.market_count() - assert n > 0 - - assert factory_2way.vaults(n - 2) == vault_long.address - assert factory_2way.vaults(n - 1) == vault_short.address - - assert factory_2way.amms(n - 2) == vault_long.amm() - assert factory_2way.amms(n - 1) == vault_short.amm() - - assert factory_2way.controllers(n - 2) == vault_long.controller() - assert factory_2way.controllers(n - 1) == vault_short.controller() - - assert factory_2way.borrowed_tokens(n - 2) == borrowed_token.address - assert factory_2way.borrowed_tokens(n - 1) == collateral_token.address - - assert factory_2way.collateral_tokens(n - 2) == vault_short.address - assert factory_2way.collateral_tokens(n - 1) == vault_long.address - - assert factory_2way.price_oracles(n - 1) != factory_2way.price_oracles(n - 2) != ZERO_ADDRESS - - # Monetary policy is NOT the same - reacts same way on utilization - assert factory_2way.monetary_policies(n - 1) != factory_2way.monetary_policies(n - 2) != ZERO_ADDRESS - - # Token index - if borrowed_token == stablecoin: - token = collateral_token - else: - token = borrowed_token - vaults = set(factory_2way.token_to_vaults(token, i) for i in range(factory_2way.token_market_count(token))) - assert vault_long.address in vaults - assert vault_short.address in vaults - - # Vaults index - assert factory_2way.vaults(factory_2way.vaults_index(vault_long.address)) == vault_long.address - assert factory_2way.vaults(factory_2way.vaults_index(vault_short.address)) == vault_short.address - - # Gauges - gauge = factory_2way.deploy_gauge(vault_long.address) - assert factory_2way.gauge_for_vault(vault_long.address) == gauge - assert factory_2way.gauges(n - 2) == gauge - gauge = factory_2way.deploy_gauge(vault_short.address) - assert factory_2way.gauge_for_vault(vault_short.address) == gauge - assert factory_2way.gauges(n - 1) == gauge - - -def test_deposit_and_withdraw(vault_long, vault_short, borrowed_token, collateral_token, accounts): - one_borrowed_token = 10 ** borrowed_token.decimals() - one_collateral_token = 10 ** collateral_token.decimals() - amount_borrowed = 10**6 * one_borrowed_token - amount_collateral = 10**6 * one_collateral_token // 3000 - user = accounts[1] - borrowed_token._mint_for_testing(user, amount_borrowed) - collateral_token._mint_for_testing(user, amount_collateral) - - with boa.env.prank(user): - borrowed_token.approve(vault_long, 2**256 - 1) - collateral_token.approve(vault_short, 2**256 - 1) - vault_long.deposit(amount_borrowed) - vault_short.deposit(amount_collateral) - - assert vault_long.totalAssets() == amount_borrowed - assert vault_long.balanceOf(user) == amount_borrowed * 10**18 * DEAD_SHARES // one_borrowed_token - assert vault_long.pricePerShare() == 10**18 // DEAD_SHARES - - assert vault_short.totalAssets() == amount_collateral - assert vault_short.balanceOf(user) == amount_collateral * 10**18 * DEAD_SHARES // one_collateral_token - assert vault_short.pricePerShare() == 10**18 // DEAD_SHARES - - vault_long.redeem(vault_long.balanceOf(user)) - vault_short.redeem(vault_short.balanceOf(user)) - - assert vault_long.totalAssets() == 0 - assert vault_short.totalAssets() == 0 diff --git a/tests/lending/test_vault.py b/tests/lending/test_vault.py index cffadab0..6d7cf355 100644 --- a/tests/lending/test_vault.py +++ b/tests/lending/test_vault.py @@ -2,48 +2,68 @@ import pytest from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant -from ..conftest import approx +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) +from tests.utils.constants import DEAD_SHARES -DEAD_SHARES = 1000 +SECONDS_PER_YEAR = 365 * 86400 -def test_vault_creation(vault, market_controller, market_amm, market_mpolicy, factory, price_oracle, - borrowed_token, collateral_token, stablecoin): - assert vault.amm() == market_amm.address - assert vault.controller() == market_controller.address - assert market_controller.monetary_policy() == market_mpolicy.address +@pytest.fixture(scope="module") +def min_borrow_rate(): + """Keep borrow APR aligned with stateful invariant (0.5% APR).""" + return (5 * 10**15) // SECONDS_PER_YEAR + + +@pytest.fixture(scope="module") +def max_borrow_rate(): + return (50 * 10**16) // SECONDS_PER_YEAR + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Override to 0""" + return 0 + + +def test_vault_creation( + vault, + controller, + amm, + monetary_policy, + factory, + price_oracle, + borrowed_token, + collateral_token, + stablecoin, +): + assert vault.amm() == amm.address + assert vault.controller() == controller.address + assert controller.monetary_policy() == monetary_policy.address n = factory.market_count() assert n > 0 assert factory.vaults(n - 1) == vault.address - assert factory.amms(n - 1) == market_amm.address - assert factory.controllers(n - 1) == market_controller.address + assert factory.amms(n - 1) == amm.address + assert factory.controllers(n - 1) == controller.address assert factory.borrowed_tokens(n - 1) == borrowed_token.address assert factory.collateral_tokens(n - 1) == collateral_token.address assert factory.price_oracles(n - 1) == price_oracle.address - assert factory.monetary_policies(n - 1) == market_mpolicy.address - - if borrowed_token == stablecoin: - token = collateral_token - else: - token = borrowed_token - vaults = set(factory.token_to_vaults(token, i) for i in range(factory.token_market_count(token))) - assert vault.address in vaults + assert factory.monetary_policies(n - 1) == monetary_policy.address assert factory.vaults(factory.vaults_index(vault.address)) == vault.address - gauge = factory.deploy_gauge(vault.address) - assert factory.gauge_for_vault(vault.address) == gauge - assert factory.gauges(n - 1) == gauge - -@pytest.mark.parametrize("supply_limit", [0, 1000 * 10**18, 2**256-1, None]) +@pytest.mark.parametrize("supply_limit", [0, 1000 * 10**18, 2**256 - 1, None]) def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_limit): one_token = 10 ** borrowed_token.decimals() amount = 10**6 * one_token user = accounts[1] - borrowed_token._mint_for_testing(user, amount) + boa.deal(borrowed_token, user, amount) if supply_limit is not None: with boa.env.prank(admin): @@ -53,7 +73,7 @@ def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_lim with boa.env.prank(user): assert vault.pricePerShare() == 10**18 // DEAD_SHARES - borrowed_token.approve(vault.address, 2**256-1) + borrowed_token.approve(vault.address, 2**256 - 1) if amount > supply_limit: with boa.reverts(): vault.deposit(amount) @@ -61,7 +81,9 @@ def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_lim vault.deposit(amount) assert vault.totalAssets() == amount assert vault.balanceOf(user) == amount * 10**18 * DEAD_SHARES // one_token - assert vault.pricePerShare() == 10**18 // DEAD_SHARES # We test different precisions here, and pps is the same + assert ( + vault.pricePerShare() == 10**18 // DEAD_SHARES + ) # We test different precisions here, and pps is the same vault.redeem(vault.balanceOf(user)) assert vault.totalAssets() == 0 @@ -69,7 +91,9 @@ def test_deposit_and_withdraw(vault, borrowed_token, accounts, admin, supply_lim class StatefulVault(RuleBasedStateMachine): user_id = st.integers(min_value=0, max_value=9) t = st.integers(min_value=0, max_value=86400 * 365) - amount = st.integers(min_value=0, max_value=10**9 * 10**18) # Would never revert - not too huge + amount = st.integers( + min_value=0, max_value=10**9 * 10**18 + ) # Would never revert - not too huge def __init__(self): super().__init__() @@ -85,7 +109,7 @@ def __init__(self): @invariant() def inv_aprs(self): if self.was_used: - assert approx(self.vault.borrow_apr() / 1e18, 0.005, 1e-5) + assert self.vault.borrow_apr() / 1e18 == pytest.approx(0.005, rel=1e-5) else: assert self.vault.borrow_apr() == 0 assert self.vault.lend_apr() == 0 @@ -98,10 +122,12 @@ def inv_total_assets(self): def inv_pps(self): pps = self.vault.pricePerShare() assert pps >= 1e18 // 1000 # Most likely we'll be around here - assert pps <= 1e18 // 1000 * 1.1 # Cannot pump much due to min assets limits (this test only pupms via rounding errors) + assert ( + pps <= 1e18 // 1000 * 1.1 + ) # Cannot pump much due to min assets limits (this test only pupms via rounding errors) if self.total_assets > 100000: if self.pps: - assert approx(pps, self.pps, 1e-2) + assert pps == pytest.approx(self.pps, rel=1e-2) else: self.pps = pps @@ -109,7 +135,7 @@ def inv_pps(self): def deposit(self, user_id, assets): assets = assets // self.precision user = self.accounts[user_id] - self.borrowed_token._mint_for_testing(user, assets) + boa.deal(self.borrowed_token, user, assets) to_mint = self.vault.previewDeposit(assets) d_vault_balance = self.vault.balanceOf(user) d_user_tokens = self.borrowed_token.balanceOf(user) @@ -133,7 +159,7 @@ def deposit_for(self, user_from, user_to, assets): assets = assets // self.precision user_from = self.accounts[user_from] user_to = self.accounts[user_to] - self.borrowed_token._mint_for_testing(user_from, assets) + boa.deal(self.borrowed_token, user_from, assets) to_mint = self.vault.previewDeposit(assets) d_vault_balance = self.vault.balanceOf(user_to) d_user_tokens = self.borrowed_token.balanceOf(user_from) @@ -156,7 +182,7 @@ def deposit_for(self, user_from, user_to, assets): def mint(self, user_id, shares): user = self.accounts[user_id] assets = self.vault.previewMint(shares) - self.borrowed_token._mint_for_testing(user, assets) + boa.deal(self.borrowed_token, user, assets) d_vault_balance = self.vault.balanceOf(user) d_user_tokens = self.borrowed_token.balanceOf(user) with boa.env.prank(user): @@ -179,7 +205,7 @@ def mint_for(self, user_from, user_to, shares): user_from = self.accounts[user_from] user_to = self.accounts[user_to] assets = self.vault.previewMint(shares) - self.borrowed_token._mint_for_testing(user_from, assets) + boa.deal(self.borrowed_token, user_from, assets) d_vault_balance = self.vault.balanceOf(user_to) d_user_tokens = self.borrowed_token.balanceOf(user_from) with boa.env.prank(user_from): @@ -206,7 +232,10 @@ def redeem(self, user_id, shares): d_vault_balance = self.vault.balanceOf(user) d_user_tokens = self.borrowed_token.balanceOf(user) with boa.env.prank(user): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.redeem(shares) return @@ -235,7 +264,10 @@ def redeem_for(self, user_from, user_to, shares): d_vault_balance = self.vault.balanceOf(user_from) d_user_tokens = self.borrowed_token.balanceOf(user_to) with boa.env.prank(user_from): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.redeem(shares, user_to) return @@ -254,7 +286,13 @@ def redeem_for(self, user_from, user_to, shares): with boa.env.prank(user_from): self.vault.redeem(shares, user_to) - @rule(user_from=user_id, user_to=user_id, owner=user_id, shares=amount, approval=amount) + @rule( + user_from=user_id, + user_to=user_id, + owner=user_id, + shares=amount, + approval=amount, + ) def redeem_owner_for(self, user_from, user_to, owner, shares, approval): if user_from == owner: return @@ -270,7 +308,10 @@ def redeem_owner_for(self, user_from, user_to, owner, shares, approval): d_vault_balance = self.vault.balanceOf(owner) d_user_tokens = self.borrowed_token.balanceOf(user_to) with boa.env.prank(user_from): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.redeem(shares, user_to, owner) return @@ -308,7 +349,10 @@ def withdraw(self, user_id, assets): d_vault_balance = self.vault.balanceOf(user) d_user_tokens = self.borrowed_token.balanceOf(user) with boa.env.prank(user): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.withdraw(assets) return @@ -337,7 +381,10 @@ def withdraw_for(self, user_from, user_to, assets): d_vault_balance = self.vault.balanceOf(user_from) d_user_tokens = self.borrowed_token.balanceOf(user_to) with boa.env.prank(user_from): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.withdraw(assets, user_to) return @@ -356,7 +403,13 @@ def withdraw_for(self, user_from, user_to, assets): with boa.env.prank(user_from): self.vault.withdraw(assets, user_to) - @rule(user_from=user_id, user_to=user_id, owner=user_id, assets=amount, approval=amount) + @rule( + user_from=user_id, + user_to=user_id, + owner=user_id, + assets=amount, + approval=amount, + ) def withdraw_owner_for(self, user_from, user_to, owner, assets, approval): if user_from == owner: return @@ -372,7 +425,10 @@ def withdraw_owner_for(self, user_from, user_to, owner, assets, approval): d_vault_balance = self.vault.balanceOf(owner) d_user_tokens = self.borrowed_token.balanceOf(user_to) with boa.env.prank(user_from): - if self.total_assets - assets < 10000 and self.total_assets - assets != 0: + if ( + self.total_assets - assets < 10000 + and self.total_assets - assets != 0 + ): with boa.reverts(): self.vault.withdraw(assets, user_to, owner) return @@ -402,7 +458,7 @@ def withdraw_owner_for(self, user_from, user_to, owner, assets, approval): self.vault.approve(user_from, 0) -def test_stateful_vault(vault, borrowed_token, accounts, admin, market_amm, market_controller): +def test_stateful_vault(vault, borrowed_token, accounts, admin, amm, controller): StatefulVault.TestCase.settings = settings(max_examples=500, stateful_step_count=50) for k, v in locals().items(): setattr(StatefulVault, k, v) diff --git a/tests_forked/__init__.py b/tests/leverage/__init__.py similarity index 100% rename from tests_forked/__init__.py rename to tests/leverage/__init__.py diff --git a/tests_forked/price_oracles/__init__.py b/tests/leverage/test_v1/__init__.py similarity index 100% rename from tests_forked/price_oracles/__init__.py rename to tests/leverage/test_v1/__init__.py diff --git a/tests_leverage/test_v1/conftest.py b/tests/leverage/test_v1/conftest.py similarity index 89% rename from tests_leverage/test_v1/conftest.py rename to tests/leverage/test_v1/conftest.py index 782228d7..23334d0c 100644 --- a/tests_leverage/test_v1/conftest.py +++ b/tests/leverage/test_v1/conftest.py @@ -2,15 +2,24 @@ import pytest from pathlib import Path from hypothesis import settings -from ape import project, accounts, Contract +from ape import project, Contract from dotenv import load_dotenv -from .utils import mint_tokens_for_testing, mint_crvusd_tokens_for_testing, ROUTER_PARAMS, ROUTER_PARAMS_DELEVERAGE, COLLATERALS, CONTROLLERS, LLAMMAS, ROUTERS +from .utils import ( + mint_tokens_for_testing, + mint_crvusd_tokens_for_testing, + ROUTER_PARAMS, + ROUTER_PARAMS_DELEVERAGE, + COLLATERALS, + CONTROLLERS, + LLAMMAS, + ROUTERS, +) BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(Path(BASE_DIR, ".env")) settings.register_profile("default", max_examples=10, deadline=None) -settings.load_profile(os.getenv(u"HYPOTHESIS_PROFILE", "default")) +settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) """ @@ -60,7 +69,9 @@ def collaterals(user): for collateral in COLLATERALS.keys(): collateral_contracts[collateral] = Contract(COLLATERALS[collateral]) # No need to give approval for WETH because we will use ETH, but it doesn't matter - collateral_contracts[collateral].approve(CONTROLLERS[collateral], 2**256 - 1, sender=user) + collateral_contracts[collateral].approve( + CONTROLLERS[collateral], 2**256 - 1, sender=user + ) return collateral_contracts diff --git a/tests_leverage/test_v1/test_deleverage.py b/tests/leverage/test_v1/test_deleverage.py similarity index 59% rename from tests_leverage/test_v1/test_deleverage.py rename to tests/leverage/test_v1/test_deleverage.py index db4bee3d..8994d06a 100644 --- a/tests_leverage/test_v1/test_deleverage.py +++ b/tests/leverage/test_v1/test_deleverage.py @@ -9,34 +9,49 @@ collateral_amt=st.floats(min_value=0.1, max_value=1000), repay_frac=st.floats(min_value=0.1, max_value=1), ) -@pytest.mark.parametrize("collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) @flaky def test_deleverage( - collaterals, - controllers, - llammas, - leverage_zaps, - deleverage_zaps, - user, - admin, - stablecoin_token, - collateral_token, - collateral_amt, - repay_frac, - route_idx, ): + collaterals, + controllers, + llammas, + leverage_zaps, + deleverage_zaps, + user, + admin, + stablecoin_token, + collateral_token, + collateral_amt, + repay_frac, + route_idx, +): with chain.isolate(): - # 1. Create leveraged position N = 10 decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, _ = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, _ = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) borrow_amt = int(max_borrowable * 0.3) - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(borrow_amt, route_idx) * 0.999) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -54,11 +69,17 @@ def test_deleverage( collateral_before, _, __, ___ = controllers[collateral_token].user_state(user) user_collateral_before = collaterals[collateral_token].balanceOf(user) - repay_collateral_amount = min(int(collateral_before * repay_frac), collateral_before) - repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins(repay_collateral_amount, route_idx) + repay_collateral_amount = min( + int(collateral_before * repay_frac), collateral_before + ) + repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins( + repay_collateral_amount, route_idx + ) _n1 = 0 if repay_debt_amount < borrow_amt: - _n1 = deleverage_zaps[collateral_token].calculate_debt_n1(repay_collateral_amount, route_idx, user) + _n1 = deleverage_zaps[collateral_token].calculate_debt_n1( + repay_collateral_amount, route_idx, user + ) controllers[collateral_token].repay_extended( deleverage_zaps[collateral_token].address, @@ -66,21 +87,32 @@ def test_deleverage( sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) n1, n2 = llammas[collateral_token].read_user_tick_numbers(user) assert stablecoin == 0 if repay_debt_amount < borrow_amt: assert collateral == collateral_before - repay_collateral_amount - assert abs((borrow_amt - debt) - repay_debt_amount) / repay_debt_amount < 1e-4 + assert ( + abs((borrow_amt - debt) - repay_debt_amount) / repay_debt_amount < 1e-4 + ) assert n1 == _n1 assert n2 == _n1 + _N - 1 else: assert collateral == 0 assert debt == 0 - assert abs((repay_debt_amount - borrow_amt) - stablecoin_token.balanceOf(user)) / stablecoin_token.balanceOf(user) < 1e-4 - assert collaterals[collateral_token].balanceOf(user) - user_collateral_before == collateral_before - repay_collateral_amount + assert ( + abs((repay_debt_amount - borrow_amt) - stablecoin_token.balanceOf(user)) + / stablecoin_token.balanceOf(user) + < 1e-4 + ) + assert ( + collaterals[collateral_token].balanceOf(user) - user_collateral_before + == collateral_before - repay_collateral_amount + ) @given( @@ -88,34 +120,48 @@ def test_deleverage( repay_frac=st.floats(min_value=0.1, max_value=1), ) # sfrxETH is in sunset mode, can't borrow max -@pytest.mark.parametrize("collateral_token", ["wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) def test_deleverage_underwater( - collaterals, - controllers, - llammas, - leverage_zaps, - deleverage_zaps, - user, - admin, - stablecoin_token, - collateral_token, - collateral_amt, - repay_frac, - route_idx, + collaterals, + controllers, + llammas, + leverage_zaps, + deleverage_zaps, + user, + admin, + stablecoin_token, + collateral_token, + collateral_amt, + repay_frac, + route_idx, ): with chain.isolate(): - # 1. Create leveraged position N = 10 decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, _ = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, _ = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) borrow_amt = max_borrowable - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(borrow_amt, route_idx) * 0.999) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -140,10 +186,14 @@ def test_deleverage_underwater( total_collateral_to_trade += llammas[collateral_token].bands_y(i) total_collateral_to_trade = total_collateral_to_trade * 10**decimals // 10**18 - llammas[collateral_token].exchange_dy(0, 1, total_collateral_to_trade, 10**26, sender=admin) + llammas[collateral_token].exchange_dy( + 0, 1, total_collateral_to_trade, 10**26, sender=admin + ) active_band = llammas[collateral_token].active_band() - collateral_before, stablecoin_before, debt_before, _N = controllers[collateral_token].user_state(user) + collateral_before, stablecoin_before, debt_before, _N = controllers[ + collateral_token + ].user_state(user) assert active_band > n1 assert active_band < n2 @@ -153,7 +203,9 @@ def test_deleverage_underwater( user_collateral_before = collaterals[collateral_token].balanceOf(user) repay_collateral_amount = int(collateral_before * repay_frac) - repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins(repay_collateral_amount, route_idx) + repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins( + repay_collateral_amount, route_idx + ) if repay_debt_amount + stablecoin_before > debt_before: # Full repayment controllers[collateral_token].repay_extended( @@ -162,11 +214,23 @@ def test_deleverage_underwater( sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) assert collateral == 0 assert debt == 0 - assert abs((repay_debt_amount + stablecoin_before - borrow_amt) - stablecoin_token.balanceOf(user)) / stablecoin_token.balanceOf(user) < 3e-4 - assert collaterals[collateral_token].balanceOf(user) - user_collateral_before == collateral_before - repay_collateral_amount + assert ( + abs( + (repay_debt_amount + stablecoin_before - borrow_amt) + - stablecoin_token.balanceOf(user) + ) + / stablecoin_token.balanceOf(user) + < 3e-4 + ) + assert ( + collaterals[collateral_token].balanceOf(user) - user_collateral_before + == collateral_before - repay_collateral_amount + ) # else: # Partial repayment reverts # with ape.reverts(""): # controllers[collateral_token].repay_extended( diff --git a/tests_leverage/test_v1/test_deleverage_light.py b/tests/leverage/test_v1/test_deleverage_light.py similarity index 60% rename from tests_leverage/test_v1/test_deleverage_light.py rename to tests/leverage/test_v1/test_deleverage_light.py index 3631bd40..f3bdf642 100644 --- a/tests_leverage/test_v1/test_deleverage_light.py +++ b/tests/leverage/test_v1/test_deleverage_light.py @@ -2,35 +2,49 @@ from ape import chain -@pytest.mark.parametrize("collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("repay_frac", [0.1, 0.5, 1]) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) def test_deleverage( - collaterals, - controllers, - llammas, - leverage_zaps, - deleverage_zaps, - user, - admin, - stablecoin_token, - collateral_token, - repay_frac, - route_idx, + collaterals, + controllers, + llammas, + leverage_zaps, + deleverage_zaps, + user, + admin, + stablecoin_token, + collateral_token, + repay_frac, + route_idx, ): with chain.isolate(): - # 1. Create leveraged position collateral_amt = 1 N = 10 decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, _ = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, _ = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) borrow_amt = int(max_borrowable * 0.3) - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(borrow_amt, route_idx) * 0.999) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -49,10 +63,14 @@ def test_deleverage( user_collateral_before = collaterals[collateral_token].balanceOf(user) repay_collateral_amount = int(collateral_before * repay_frac) - repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins(repay_collateral_amount, route_idx) + repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins( + repay_collateral_amount, route_idx + ) _n1 = 0 if repay_debt_amount < borrow_amt: - _n1 = deleverage_zaps[collateral_token].calculate_debt_n1(repay_collateral_amount, route_idx, user) + _n1 = deleverage_zaps[collateral_token].calculate_debt_n1( + repay_collateral_amount, route_idx, user + ) controllers[collateral_token].repay_extended( deleverage_zaps[collateral_token].address, @@ -60,53 +78,78 @@ def test_deleverage( sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) n1, n2 = llammas[collateral_token].read_user_tick_numbers(user) assert stablecoin == 0 if repay_debt_amount < borrow_amt: assert collateral == collateral_before - repay_collateral_amount - assert abs((borrow_amt - debt) - repay_debt_amount) / repay_debt_amount < 1e-4 + assert ( + abs((borrow_amt - debt) - repay_debt_amount) / repay_debt_amount < 1e-4 + ) assert n1 == _n1 assert n2 == _n1 + _N - 1 else: assert collateral == 0 assert debt == 0 - assert abs((repay_debt_amount - borrow_amt) - stablecoin_token.balanceOf(user)) / stablecoin_token.balanceOf(user) < 1e-4 - assert collaterals[collateral_token].balanceOf(user) - user_collateral_before == collateral_before - repay_collateral_amount + assert ( + abs((repay_debt_amount - borrow_amt) - stablecoin_token.balanceOf(user)) + / stablecoin_token.balanceOf(user) + < 1e-4 + ) + assert ( + collaterals[collateral_token].balanceOf(user) - user_collateral_before + == collateral_before - repay_collateral_amount + ) # sfrxETH is in sunset mode, can't borrow max -@pytest.mark.parametrize("collateral_token", ["wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("repay_frac", [0.7, 0.8, 0.9, 1]) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) def test_deleverage_underwater( - collaterals, - controllers, - llammas, - leverage_zaps, - deleverage_zaps, - user, - admin, - collateral_token, - stablecoin_token, - repay_frac, - route_idx, + collaterals, + controllers, + llammas, + leverage_zaps, + deleverage_zaps, + user, + admin, + collateral_token, + stablecoin_token, + repay_frac, + route_idx, ): with chain.isolate(): - # 1. Create leveraged position collateral_amt = 1 N = 10 decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, _ = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, _ = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) borrow_amt = max_borrowable - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(borrow_amt, route_idx) * 0.999) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -131,10 +174,14 @@ def test_deleverage_underwater( total_collateral_to_trade += llammas[collateral_token].bands_y(i) total_collateral_to_trade = total_collateral_to_trade * 10**decimals // 10**18 - llammas[collateral_token].exchange_dy(0, 1, total_collateral_to_trade, 10**26, sender=admin) + llammas[collateral_token].exchange_dy( + 0, 1, total_collateral_to_trade, 10**26, sender=admin + ) active_band = llammas[collateral_token].active_band() - collateral_before, stablecoin_before, debt_before, _N = controllers[collateral_token].user_state(user) + collateral_before, stablecoin_before, debt_before, _N = controllers[ + collateral_token + ].user_state(user) assert active_band > n1 assert active_band < n2 @@ -144,7 +191,9 @@ def test_deleverage_underwater( user_collateral_before = collaterals[collateral_token].balanceOf(user) repay_collateral_amount = int(collateral_before * repay_frac) - repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins(repay_collateral_amount, route_idx) + repay_debt_amount = deleverage_zaps[collateral_token].get_stablecoins( + repay_collateral_amount, route_idx + ) if repay_debt_amount + stablecoin_before > debt_before: # Full repayment controllers[collateral_token].repay_extended( @@ -153,11 +202,23 @@ def test_deleverage_underwater( sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) assert collateral == 0 assert debt == 0 - assert abs((repay_debt_amount + stablecoin_before - borrow_amt) - stablecoin_token.balanceOf(user)) / stablecoin_token.balanceOf(user) < 3e-4 - assert collaterals[collateral_token].balanceOf(user) - user_collateral_before == collateral_before - repay_collateral_amount + assert ( + abs( + (repay_debt_amount + stablecoin_before - borrow_amt) + - stablecoin_token.balanceOf(user) + ) + / stablecoin_token.balanceOf(user) + < 3e-4 + ) + assert ( + collaterals[collateral_token].balanceOf(user) - user_collateral_before + == collateral_before - repay_collateral_amount + ) # else: # Partial repayment reverts # with ape.reverts(""): # controllers[collateral_token].repay_extended( diff --git a/tests_leverage/test_v1/test_leverage.py b/tests/leverage/test_v1/test_leverage.py similarity index 61% rename from tests_leverage/test_v1/test_leverage.py rename to tests/leverage/test_v1/test_leverage.py index 0cbd1e85..e7eccb33 100644 --- a/tests_leverage/test_v1/test_leverage.py +++ b/tests/leverage/test_v1/test_leverage.py @@ -10,23 +10,51 @@ borrow_amt=st.integers(min_value=10**18, max_value=10**25), N=st.integers(min_value=4, max_value=50), ) -@pytest.mark.parametrize("collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) @flaky -def test_leverage(collaterals, controllers, llammas, leverage_zaps, user, collateral_amt, borrow_amt, N, route_idx, collateral_token): +def test_leverage( + collaterals, + controllers, + llammas, + leverage_zaps, + user, + collateral_amt, + borrow_amt, + N, + route_idx, + collateral_token, +): with chain.isolate(): decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, max_collateral = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, max_collateral = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) assert max_borrowable > 0 _borrow_amt = min(max_borrowable, borrow_amt) if _borrow_amt == max_borrowable: expected_collateral = max_collateral else: - expected_collateral = _collateral_amt + leverage_zaps[collateral_token].get_collateral(_borrow_amt, route_idx) - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, _borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(_borrow_amt, route_idx) * 0.999) + expected_collateral = _collateral_amt + leverage_zaps[ + collateral_token + ].get_collateral(_borrow_amt, route_idx) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, _borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + _borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -39,7 +67,9 @@ def test_leverage(collaterals, controllers, llammas, leverage_zaps, user, collat sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) n1, n2 = llammas[collateral_token].read_user_tick_numbers(user) assert abs((expected_collateral - collateral) / collateral) < 2e-7 diff --git a/tests_leverage/test_v1/test_leverage_light.py b/tests/leverage/test_v1/test_leverage_light.py similarity index 59% rename from tests_leverage/test_v1/test_leverage_light.py rename to tests/leverage/test_v1/test_leverage_light.py index 64fd7905..e704c4af 100644 --- a/tests_leverage/test_v1/test_leverage_light.py +++ b/tests/leverage/test_v1/test_leverage_light.py @@ -2,25 +2,51 @@ from ape import chain -@pytest.mark.parametrize("collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"]) +@pytest.mark.parametrize( + "collateral_token", ["sfrxETH", "wstETH", "WBTC", "WETH", "sfrxETH2", "tBTC"] +) @pytest.mark.parametrize("borrow_amt", [10_000 * 10**18, 2**256 - 1]) @pytest.mark.parametrize("route_idx", [0, 1, 2, 3, 4]) -def test_leverage(collaterals, controllers, llammas, leverage_zaps, user, collateral_token, borrow_amt, route_idx): +def test_leverage( + collaterals, + controllers, + llammas, + leverage_zaps, + user, + collateral_token, + borrow_amt, + route_idx, +): with chain.isolate(): collateral_amt = 1 N = 10 decimals = collaterals[collateral_token].decimals() - balance = user.balance if collateral_token == "WETH" else collaterals[collateral_token].balanceOf(user) + balance = ( + user.balance + if collateral_token == "WETH" + else collaterals[collateral_token].balanceOf(user) + ) _collateral_amt = min(int(collateral_amt * 10**decimals), balance) - max_borrowable, max_collateral = leverage_zaps[collateral_token].max_borrowable_and_collateral(_collateral_amt, N, route_idx) + max_borrowable, max_collateral = leverage_zaps[ + collateral_token + ].max_borrowable_and_collateral(_collateral_amt, N, route_idx) assert max_borrowable > 0 _borrow_amt = min(max_borrowable, borrow_amt) if _borrow_amt == max_borrowable: expected_collateral = max_collateral else: - expected_collateral = _collateral_amt + leverage_zaps[collateral_token].get_collateral(_borrow_amt, route_idx) - _n1 = leverage_zaps[collateral_token].calculate_debt_n1(_collateral_amt, _borrow_amt, N, route_idx) - min_recv = int(leverage_zaps[collateral_token].get_collateral_underlying(_borrow_amt, route_idx) * 0.999) + expected_collateral = _collateral_amt + leverage_zaps[ + collateral_token + ].get_collateral(_borrow_amt, route_idx) + _n1 = leverage_zaps[collateral_token].calculate_debt_n1( + _collateral_amt, _borrow_amt, N, route_idx + ) + min_recv = int( + leverage_zaps[collateral_token].get_collateral_underlying( + _borrow_amt, route_idx + ) + * 0.999 + ) value = _collateral_amt if collateral_token == "WETH" else 0 controllers[collateral_token].create_loan_extended( @@ -33,7 +59,9 @@ def test_leverage(collaterals, controllers, llammas, leverage_zaps, user, collat sender=user, ) - collateral, stablecoin, debt, _N = controllers[collateral_token].user_state(user) + collateral, stablecoin, debt, _N = controllers[collateral_token].user_state( + user + ) n1, n2 = llammas[collateral_token].read_user_tick_numbers(user) assert abs((expected_collateral - collateral) / collateral) < 2e-7 diff --git a/tests/leverage/test_v1/utils.py b/tests/leverage/test_v1/utils.py new file mode 100644 index 00000000..830ec574 --- /dev/null +++ b/tests/leverage/test_v1/utils.py @@ -0,0 +1,1766 @@ +from ape import Contract, Project, accounts + + +def mint_tokens_for_testing(project: Project, account): + """ + Provides given account with 100 WBTC and 1000 ETH, sfrxETH, wstETH + Can be used only on local forked mainnet + + :return: None + """ + + # WBTC + token_contract = Contract("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") + token_owner = "0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656" + project.provider.set_balance(token_owner, 10**20) + amount = 100 * 10**8 + token_contract.transfer(account, amount, sender=accounts[token_owner]) + assert token_contract.balanceOf(account.address) >= amount + + # tBTC + token_contract = Contract("0x18084fbA666a33d37592fA2633fD49a74DD93a88") + token_owner = ( + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" # Wormhole: Portal Token Bridge + ) + project.provider.set_balance(token_owner, 10**20) + amount = 100 * 10**18 + token_contract.transfer(account, amount, sender=accounts[token_owner]) + assert token_contract.balanceOf(account.address) >= amount + + # ETH + # Set balance to twice amount + 1 - half will be wrapped + (potential) gas + project.provider.set_balance(account.address, 1000 * 10**18) + assert account.balance >= 1000 * 10**18 + + # sfrxETH + token_contract = Contract("0xac3e018457b222d93114458476f3e3416abbe38f") + token_owner = "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + project.provider.set_balance(token_owner, 10**20) + amount = 1000 * 10**18 + token_contract.transfer(account, amount, sender=accounts[token_owner]) + assert token_contract.balanceOf(account.address) >= amount + + # wstETH + token_contract = Contract("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0") + token_owner = "0x0B925eD163218f6662a35e0f0371Ac234f9E9371" + project.provider.set_balance(token_owner, 10**20) + amount = 1000 * 10**18 + token_contract.transfer(account, amount, sender=accounts[token_owner]) + assert token_contract.balanceOf(account.address) >= amount + + +def mint_crvusd_tokens_for_testing(project: Project, account): + """ + Provides given account with 100M crvUSD + Can be used only on local forked mainnet + + :return: None + """ + + # crvUSD + token_contract = Contract("0xf939e0a03fb07f59a73314e73794be0e57ac1b4e") + token_owner = "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635" # WETH controller + project.provider.set_balance(token_owner, 10**20) + amount = 100_000_000 * 10**18 + token_contract.transfer(account, amount, sender=accounts[token_owner]) + assert token_contract.balanceOf(account.address) >= amount + + +CRVUSD = "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e" + +frxETH = "0x5E8422345238F34275888049021821E8E08CAa1f" +stETH = "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + +COLLATERALS = { + "sfrxETH": "0xac3E018457B222d93114458476f3E3416Abbe38F", + "wstETH": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "WBTC": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "sfrxETH2": "0xac3E018457B222d93114458476f3E3416Abbe38F", + "tBTC": "0x18084fba666a33d37592fa2633fd49a74dd93a88", +} + +CONTROLLERS = { + "sfrxETH": "0x8472A9A7632b173c8Cf3a86D3afec50c35548e76", + "wstETH": "0x100daa78fc509db39ef7d04de0c1abd299f4c6ce", + "WBTC": "0x4e59541306910ad6dc1dac0ac9dfb29bd9f15c67", + "WETH": "0xa920de414ea4ab66b97da1bfe9e6eca7d4219635", + "sfrxETH2": "0xec0820efafc41d8943ee8de495fc9ba8495b15cf", + "tBTC": "0x1c91da0223c763d2e0173243eadaa0a2ea47e704", +} + +LLAMMAS = { + "sfrxETH": "0x136e783846ef68c8bd00a3369f787df8d683a696", + "wstETH": "0x37417b2238aa52d0dd2d6252d989e728e8f706e4", + "WBTC": "0xe0438eb3703bf871e31ce639bd351109c88666ea", + "WETH": "0x1681195c176239ac5e72d9aebacf5b2492e0c4ee", + "sfrxETH2": "0xfa96ad0a9e64261db86950e2da362f5572c5c6fd", + "tBTC": "0xf9bd9da2427a50908c4c6d1599d8e62837c2bcb0", +} + +ROUTERS = { + "sfrxETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", + "wstETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", + "WBTC": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", + "WETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", + "sfrxETH2": "0xF0d4c12A5768D806021F80a262B4d39d26C58b8D", + "tBTC": "0xF0d4c12A5768D806021F80a262B4d39d26C58b8D", +} + +CRVUSD_POOLS = { + "USDC": "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", + "USDT": "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4", + "USDP": "0xCa978A0528116DDA3cbA9ACD3e68bc6191CA53D0", + "TUSD": "0x34D655069F4cAc1547E4C8cA284FfFF5ad4A8db0", + "FRAX": "0x0cd6f267b2086bea681e922e19d40512511be538", +} + +ROUTER_PARAMS = { + "sfrxETH": { + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> frxeth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + frxETH, + ], + "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2 -> frxeth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + frxETH, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> frxeth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDP"], + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + frxETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> frxeth", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + frxETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2 -> frxeth", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + frxETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "wstETH": { + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> steth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + stETH, + ], + "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2 -> steth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + stETH, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> steth", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDP"], + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + stETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> steth", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + stETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2 -> steth", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + stETH, + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "WBTC": { + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [1, 2, 1], [0, 1, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 1, 3], [0, 0, 0], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDP"], + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "WETH": { + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 2, 3], [0, 0, 0], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDP"], + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "sfrxETH2": { + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", + COLLATERALS["sfrxETH"], + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 2, 1, 1, 3], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["USDC"], + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", + COLLATERALS["sfrxETH"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["USDT"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDP"], + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", + COLLATERALS["sfrxETH"], + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["USDP"], + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", + COLLATERALS["sfrxETH"], + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["TUSD"], + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xbafa44efe7901e04e39dad13167d089c559c1138", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xac3e018457b222d93114458476f3e3416abbe38f", + COLLATERALS["sfrxETH"], + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 2, 1, 3, 3], + [0, 0, 8, 0, 0], + [0, 0, 8, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["FRAX"], + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "tBTC": { + "tbtc": { + "name": "factory-tricrypto-2 (TricryptoLLAMA)", + "route": [ + CRVUSD, + "0x2889302a794da87fbf1d6db415c1492194663d13", + COLLATERALS["tBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 1, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x2889302a794da87fbf1d6db415c1492194663d13", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdc": { + "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDC"], + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + COLLATERALS["tBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 2, 1, 1, 3], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["USDC"], + "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "crvUSD/USDT -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", + "route": [ + CRVUSD, + CRVUSD_POOLS["USDT"], + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + COLLATERALS["tBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["USDT"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", + "route": [ + CRVUSD, + CRVUSD_POOLS["TUSD"], + "0x0000000000085d4780b73119b644ae5ecd22b376", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + COLLATERALS["tBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["TUSD"], + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "crvUSD/FRAX -> frax -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", + "route": [ + CRVUSD, + CRVUSD_POOLS["FRAX"], + "0x853d955acef822db058eb8505911ed77f175b99e", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + COLLATERALS["WBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + COLLATERALS["tBTC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [0, 3, 2, 1, 4], + [0, 1, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + CRVUSD_POOLS["FRAX"], + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x0000000000000000000000000000000000000000", + ], + }, + }, +} + +ROUTER_PARAMS_DELEVERAGE = { + "sfrxETH": { + "usdc": { + "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + ], + }, + "tricrv": { + "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + ], + }, + "frax": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + ], + }, + }, + "wstETH": { + "usdc": { + "name": "wstETH wrapper -> steth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["wstETH"], + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "wstETH wrapper -> steth -> tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["wstETH"], + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + ], + }, + "tricrv": { + "name": "wstETH wrapper -> steth -> factory-tricrypto-4 (TriCRV)", + "route": [ + COLLATERALS["wstETH"], + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "wstETH wrapper -> steth -> tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["wstETH"], + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + ], + }, + "frax": { + "name": "wstETH wrapper -> steth -> tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["wstETH"], + "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xdc24316b9ae028f1497c275eb9192a3ea0f67022", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + ], + }, + }, + "WBTC": { + "usdc": { + "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["WBTC"], + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["WBTC"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["WBTC"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdp": { + "name": "tricrypto2 -> factory-v2-59 (USDP) -> crvUSD/USDP", + "route": [ + COLLATERALS["WBTC"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + CRVUSD_POOLS["USDP"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xc270b3b858c335b6ba5d5b10e2da8a09976005ad", + CRVUSD_POOLS["USDP"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["WBTC"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "WETH": { + "usdc": { + "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["WETH"], + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["WETH"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tricrv": { + "name": "factory-tricrypto-4 (TriCRV)", + "route": [ + COLLATERALS["WETH"], + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["WETH"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["WETH"], + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + }, + "sfrxETH2": { + "usdc": { + "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + ], + }, + "tricrv": { + "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + ], + }, + "frax": { + "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["sfrxETH"], + "0xac3e018457b222d93114458476f3e3416abbe38f", + "0x5e8422345238f34275888049021821e8e08caa1f", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + ], + "swap_params": [ + [0, 0, 8, 0, 0], + [1, 0, 1, 1, 2], + [2, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + ], + "factory_swap_addresses": [ + "0x0000000000000000000000000000000000000000", + "0xa1f8a6807c402e4a15ef4eba36528a3fed24e577", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + ], + }, + }, + "tBTC": { + "tbtc": { + "name": "factory-tricrypto-2 (TricryptoLLAMA)", + "route": [ + COLLATERALS["tBTC"], + "0x2889302a794da87fbf1d6db415c1492194663d13", + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 3, 3], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0x2889302a794da87fbf1d6db415c1492194663d13", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdc": { + "name": "factory-crvusd-16 (tBTC/WBTC) -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", + "route": [ + COLLATERALS["tBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + CRVUSD_POOLS["USDC"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x7f86bf177dd4f3494b841a37e810a34dd56c829b", + CRVUSD_POOLS["USDC"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "usdt": { + "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> crvUSD/USDT", + "route": [ + COLLATERALS["tBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + CRVUSD_POOLS["USDT"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + CRVUSD_POOLS["USDT"], + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + }, + "tusd": { + "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> tusd -> crvUSD/TUSD", + "route": [ + COLLATERALS["tBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + "0x0000000000085d4780b73119b644ae5ecd22b376", + CRVUSD_POOLS["TUSD"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xecd5e75afb02efa118af914515d6521aabd189f1", + CRVUSD_POOLS["TUSD"], + "0x0000000000000000000000000000000000000000", + ], + }, + "frax": { + "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> frax -> crvUSD/FRAX", + "route": [ + COLLATERALS["tBTC"], + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xdac17f958d2ee523a2206206994597c13d831ec7", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + "0x853d955acef822db058eb8505911ed77f175b99e", + CRVUSD_POOLS["FRAX"], + CRVUSD, + "0x0000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000", + ], + "swap_params": [ + [1, 0, 1, 1, 2], + [1, 0, 1, 3, 3], + [3, 0, 2, 1, 4], + [0, 1, 1, 1, 2], + [0, 0, 0, 0, 0], + ], + "factory_swap_addresses": [ + "0xb7ecb2aa52aa64a717180e030241bc75cd946726", + "0xd51a44d3fae010294c616388b506acda1bfaae46", + "0xd632f22692fac7611d2aa1c0d552930d43caed3b", + CRVUSD_POOLS["FRAX"], + "0x0000000000000000000000000000000000000000", + ], + }, + }, +} diff --git a/tests_forked_ape/__init__.py b/tests/leverage/test_v2/__init__.py similarity index 100% rename from tests_forked_ape/__init__.py rename to tests/leverage/test_v2/__init__.py diff --git a/tests_leverage/test_v2/conftest.py b/tests/leverage/test_v2/conftest.py similarity index 82% rename from tests_leverage/test_v2/conftest.py rename to tests/leverage/test_v2/conftest.py index dc83d8e3..65cd8c53 100644 --- a/tests_leverage/test_v2/conftest.py +++ b/tests/leverage/test_v2/conftest.py @@ -3,14 +3,16 @@ from .constants import COLLATERALS, CONTROLLERS, CRVUSD, FACTORIES, ROUTER from .constants import frxETH as frxETH_address -from .constants import stETH as stETH_address from .settings import WEB3_PROVIDER_URL from .utils import Router1inch, get_contract_from_explorer def pytest_generate_tests(metafunc): if "collateral_info" in metafunc.fixturenames: - collaterals = [{"address": v, "controller": CONTROLLERS[k], "symbol": k} for k, v in COLLATERALS.items()] + collaterals = [ + {"address": v, "controller": CONTROLLERS[k], "symbol": k} + for k, v in COLLATERALS.items() + ] metafunc.parametrize( "collateral_info", @@ -21,7 +23,9 @@ def pytest_generate_tests(metafunc): @pytest.fixture(autouse=True) def forked_chain(): - assert WEB3_PROVIDER_URL is not None, "Provider url is not set, add WEB3_PROVIDER_URL param to env" + assert WEB3_PROVIDER_URL is not None, ( + "Provider url is not set, add WEB3_PROVIDER_URL param to env" + ) boa.env.fork(url=WEB3_PROVIDER_URL) @@ -32,7 +36,9 @@ def alice(collateral_info): with boa.env.prank(user): if collateral_info["symbol"] == "sfrxETH": - swap = get_contract_from_explorer("0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577") + swap = get_contract_from_explorer( + "0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577" + ) swap.exchange(0, 1, 2 * 10**18, 1 * 10**18, value=2 * 10**18) sfrxETH = get_contract_from_explorer(collateral_info["address"]) frxETH = get_contract_from_explorer(frxETH_address) diff --git a/tests_leverage/test_v2/constants.py b/tests/leverage/test_v2/constants.py similarity index 100% rename from tests_leverage/test_v2/constants.py rename to tests/leverage/test_v2/constants.py diff --git a/tests_leverage/test_v2/settings.py b/tests/leverage/test_v2/settings.py similarity index 100% rename from tests_leverage/test_v2/settings.py rename to tests/leverage/test_v2/settings.py diff --git a/tests_leverage/__init__.py b/tests/leverage/test_v2/tests/__init__.py similarity index 100% rename from tests_leverage/__init__.py rename to tests/leverage/test_v2/tests/__init__.py diff --git a/tests/leverage/test_v2/tests/test_leverage.py b/tests/leverage/test_v2/tests/test_leverage.py new file mode 100644 index 00000000..0791c0c4 --- /dev/null +++ b/tests/leverage/test_v2/tests/test_leverage.py @@ -0,0 +1,32 @@ +class TestLeverage: + class TestCreateLoan: + def test_max_borrowable( + self, alice, collateral, controller, leverage_zap_1inch, router_api_1inch + ): + N = 4 + max_collateral = 0 + collateral_decimals = 10 ** collateral.decimals() + p_avg = ( + 10**18 + * collateral_decimals + // router_api_1inch.get_rate(collateral.address, 1 * 10**18) + ) + balance = ( + 1 * 10**18 + if collateral.symbol() == "WETH" + else collateral.balanceOf(alice) + ) + + max_borrowable = leverage_zap_1inch.max_borrowable( + controller.address, balance, max_collateral, N, p_avg + ) + + max_collateral = router_api_1inch.get_rate( + collateral.address, max_borrowable + ) + p_avg = max_borrowable * collateral_decimals // max_collateral + max_borrowable = leverage_zap_1inch.max_borrowable( + controller.address, balance, max_collateral, N, p_avg + ) + + assert max_borrowable > 0 diff --git a/tests_leverage/test_v2/utils.py b/tests/leverage/test_v2/utils.py similarity index 84% rename from tests_leverage/test_v2/utils.py rename to tests/leverage/test_v2/utils.py index ed2f7bdc..d3eebc0e 100644 --- a/tests_leverage/test_v2/utils.py +++ b/tests/leverage/test_v2/utils.py @@ -6,7 +6,9 @@ def get_contract_from_explorer(address: str): - return boa.from_etherscan(address=address, name=address, uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) + return boa.from_etherscan( + address=address, name=address, uri=EXPLORER_URL, api_key=EXPLORER_TOKEN + ) class Router1inch: diff --git a/tests/lm_callback/conftest.py b/tests/lm_callback/conftest.py index cf9daf05..fdca8501 100644 --- a/tests/lm_callback/conftest.py +++ b/tests/lm_callback/conftest.py @@ -1,25 +1,41 @@ import boa import pytest +from tests.utils.deployers import ( + ERC20_CRV_DEPLOYER, + VOTING_ESCROW_DEPLOYER, + GAUGE_CONTROLLER_DEPLOYER, + MINTER_DEPLOYER, + STABLECOIN_DEPLOYER, + WETH_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + AMM_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + LM_CALLBACK_DEPLOYER, + BLOCK_COUNTER_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, +) @pytest.fixture(scope="module") def crv(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20CRV.vy', "Curve DAO Token", "CRV", 18) + return ERC20_CRV_DEPLOYER.deploy("Curve DAO Token", "CRV", 18) @pytest.fixture(scope="module") def voting_escrow(admin, crv): with boa.env.prank(admin): - return boa.load('contracts/testing/VotingEscrow.vy', crv, "Voting-escrowed CRV", "veCRV", "veCRV_0.99") + return VOTING_ESCROW_DEPLOYER.deploy( + crv, "Voting-escrowed CRV", "veCRV", "veCRV_0.99" + ) @pytest.fixture(scope="module") def gauge_controller(admin, crv, voting_escrow): with boa.env.prank(admin): - gauge_controller = boa.load('contracts/testing/GaugeController.vy', crv, voting_escrow) + gauge_controller = GAUGE_CONTROLLER_DEPLOYER.deploy(crv, voting_escrow) gauge_controller.add_type("crvUSD Market") - gauge_controller.change_type_weight(0, 10 ** 18) + gauge_controller.change_type_weight(0, 10**18) return gauge_controller @@ -27,7 +43,7 @@ def gauge_controller(admin, crv, voting_escrow): @pytest.fixture(scope="module") def minter(admin, crv, gauge_controller): with boa.env.prank(admin): - _minter = boa.load('contracts/testing/Minter.vy', crv, gauge_controller) + _minter = MINTER_DEPLOYER.deploy(crv, gauge_controller) crv.set_minter(_minter) return _minter @@ -36,7 +52,7 @@ def minter(admin, crv, gauge_controller): @pytest.fixture(scope="module") def chad(collateral_token, admin): _chad = boa.env.generate_address() - collateral_token._mint_for_testing(_chad, 10**25, sender=admin) + boa.deal(collateral_token, _chad, 10**25) return _chad @@ -44,7 +60,7 @@ def chad(collateral_token, admin): @pytest.fixture(scope="module") def stablecoin(admin, chad): with boa.env.prank(admin): - _stablecoin = boa.load('contracts/Stablecoin.vy', 'Curve USD', 'crvUSD') + _stablecoin = STABLECOIN_DEPLOYER.deploy("Curve USD", "crvUSD") _stablecoin.mint(chad, 10**25) return _stablecoin @@ -53,41 +69,36 @@ def stablecoin(admin, chad): @pytest.fixture(scope="module") def weth(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/WETH.vy') + return WETH_DEPLOYER.deploy() @pytest.fixture(scope="module") def controller_prefactory(stablecoin, weth, admin, accounts): with boa.env.prank(admin): - return boa.load('contracts/ControllerFactory.vy', stablecoin.address, admin, admin, weth.address) + return CONTROLLER_FACTORY_DEPLOYER.deploy( + stablecoin.address, admin, admin, weth.address + ) @pytest.fixture(scope="module") -def controller_interface(): - return boa.load_partial('contracts/Controller.vy') +def controller_impl(proto): + return proto.blueprints.mint_controller @pytest.fixture(scope="module") -def controller_impl(controller_interface, admin): +def amm_impl(stablecoin, admin): with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() + return AMM_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="module") -def amm_interface(): - return boa.load_partial('contracts/AMM.vy') - - -@pytest.fixture(scope="module") -def amm_impl(stablecoin, amm_interface, admin): - with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() - - -@pytest.fixture(scope="module") -def controller_factory(controller_prefactory, amm_impl, controller_impl, stablecoin, admin): +def controller_factory( + controller_prefactory, amm_impl, controller_impl, stablecoin, admin +): with boa.env.prank(admin): - controller_prefactory.set_implementations(controller_impl.address, amm_impl.address) + controller_prefactory.set_implementations( + controller_impl.address, amm_impl.address + ) stablecoin.set_minter(controller_prefactory.address) return controller_prefactory @@ -95,33 +106,42 @@ def controller_factory(controller_prefactory, amm_impl, controller_impl, stablec @pytest.fixture(scope="module") def monetary_policy(admin): with boa.env.prank(admin): - policy = boa.load('contracts/testing/ConstantMonetaryPolicy.vy', admin) + policy = CONSTANT_MONETARY_POLICY_DEPLOYER.deploy(admin) policy.set_rate(0) return policy @pytest.fixture(scope="module") -def get_market(controller_factory, monetary_policy, price_oracle, stablecoin, accounts, admin, chad): +def get_market( + controller_factory, monetary_policy, price_oracle, stablecoin, accounts, admin, chad +): def f(collateral_token): with boa.env.prank(admin): if controller_factory.n_collaterals() == 0: controller_factory.add_market( - collateral_token.address, 100, 10**16, 0, + collateral_token.address, + 100, + 10**16, + 0, price_oracle.address, - monetary_policy.address, 5 * 10**16, 2 * 10**16, - 10**8 * 10**18) + monetary_policy.address, + 5 * 10**16, + 2 * 10**16, + 10**8 * 10**18, + ) amm = controller_factory.get_amm(collateral_token.address) controller = controller_factory.get_controller(collateral_token.address) for acc in accounts: with boa.env.prank(acc): - collateral_token.approve(amm, 2**256-1) - stablecoin.approve(amm, 2**256-1) - collateral_token.approve(controller, 2**256-1) - stablecoin.approve(controller, 2**256-1) + collateral_token.approve(amm, 2**256 - 1) + stablecoin.approve(amm, 2**256 - 1) + collateral_token.approve(controller, 2**256 - 1) + stablecoin.approve(controller, 2**256 - 1) with boa.env.prank(chad): - collateral_token.approve(amm, 2 ** 256 - 1) - stablecoin.approve(amm, 2 ** 256 - 1) + collateral_token.approve(amm, 2**256 - 1) + stablecoin.approve(amm, 2**256 - 1) return controller_factory + return f @@ -131,22 +151,34 @@ def market(get_market, collateral_token): @pytest.fixture(scope="module") -def market_amm(market, collateral_token, stablecoin, amm_interface, accounts): - return amm_interface.at(market.get_amm(collateral_token.address)) +def market_amm(market, collateral_token, stablecoin, accounts): + return AMM_DEPLOYER.at(market.get_amm(collateral_token.address)) @pytest.fixture(scope="module") -def market_controller(market, stablecoin, collateral_token, controller_interface, controller_factory, accounts): - return controller_interface.at(market.get_controller(collateral_token.address)) +def market_controller( + market, stablecoin, collateral_token, controller_factory, accounts +): + return LEND_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) @pytest.fixture(scope="module") -def lm_callback(admin, market_amm, crv, gauge_controller, minter, market_controller, controller_factory): +def lm_callback( + admin, + market_amm, + crv, + gauge_controller, + minter, + market_controller, + controller_factory, +): with boa.env.prank(admin): - cb = boa.load('contracts/LMCallback.vy', market_amm, crv, gauge_controller, minter, controller_factory) + cb = LM_CALLBACK_DEPLOYER.deploy( + market_amm, crv, gauge_controller, minter, controller_factory + ) market_controller.set_callback(cb) # Wire up LM Callback to the gauge controller to have proper rates and stuff - gauge_controller.add_gauge(cb.address, 0, 10 ** 18) + gauge_controller.add_gauge(cb.address, 0, 10**18) return cb @@ -154,4 +186,4 @@ def lm_callback(admin, market_amm, crv, gauge_controller, minter, market_control @pytest.fixture(scope="module") def block_counter(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/BlockCounter.vy') + return BLOCK_COUNTER_DEPLOYER.deploy() diff --git a/tests/lm_callback/test_add_new_lm_callback.py b/tests/lm_callback/test_add_new_lm_callback.py index 2ff09979..7704d4cf 100644 --- a/tests/lm_callback/test_add_new_lm_callback.py +++ b/tests/lm_callback/test_add_new_lm_callback.py @@ -1,37 +1,41 @@ import boa +from tests.utils.deployers import LM_CALLBACK_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS WEEK = 7 * 86400 def test_add_new_lm_callback( - accounts, - admin, - collateral_token, - crv, - market_controller, - market_amm, - minter, - gauge_controller, - controller_factory + accounts, + admin, + collateral_token, + crv, + market_controller, + market_amm, + minter, + gauge_controller, + controller_factory, ): alice, bob = accounts[:2] # Remove current LM Callback - market_controller.set_callback("0x0000000000000000000000000000000000000000", sender=admin) + market_controller.set_callback(ZERO_ADDRESS, sender=admin) boa.env.time_travel(seconds=2 * WEEK + 5) # Create loan - collateral_token._mint_for_testing(alice, 10**22, sender=admin) + boa.deal(collateral_token, alice, 10**22) market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) - collateral_token._mint_for_testing(bob, 10**22, sender=admin) + boa.deal(collateral_token, bob, 10**22) market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=bob) # Wire up the new LM Callback to the gauge controller to have proper rates and stuff with boa.env.prank(admin): - new_cb = boa.load('contracts/LMCallback.vy', market_amm, crv, gauge_controller, minter, controller_factory) + new_cb = LM_CALLBACK_DEPLOYER.deploy( + market_amm, crv, gauge_controller, minter, controller_factory + ) market_controller.set_callback(new_cb) - gauge_controller.add_gauge(new_cb.address, 0, 10 ** 18) + gauge_controller.add_gauge(new_cb.address, 0, 10**18) boa.env.time_travel(WEEK) new_cb.user_checkpoint(alice, sender=alice) @@ -45,7 +49,7 @@ def test_add_new_lm_callback( assert rewards == 0 # Bob interacts with the market - market_controller.borrow_more(0, 10 ** 18, sender=bob) + market_controller.borrow_more(0, 10**18, sender=bob) boa.env.time_travel(WEEK) new_cb.user_checkpoint(alice, sender=alice) @@ -55,5 +59,5 @@ def test_add_new_lm_callback( collateral_from_amm = market_controller.user_state(alice)[0] collateral_from_cb = new_cb.user_collateral(alice) - assert collateral_from_cb == collateral_from_amm == 10 ** 21 + assert collateral_from_cb == collateral_from_amm == 10**21 assert rewards > 0 diff --git a/tests/lm_callback/test_as_gauge.py b/tests/lm_callback/test_as_gauge.py index 69ee439e..3ec45f23 100644 --- a/tests/lm_callback/test_as_gauge.py +++ b/tests/lm_callback/test_as_gauge.py @@ -1,35 +1,44 @@ import boa from random import random, randrange -from ..conftest import approx +import pytest -MAX_UINT256 = 2 ** 256 - 1 +MAX_UINT256 = 2**256 - 1 YEAR = 365 * 86400 WEEK = 7 * 86400 -def test_gauge_integral_one_user(accounts, admin, collateral_token, crv, lm_callback, market_controller, minter): +def test_gauge_integral_one_user( + accounts, admin, collateral_token, crv, lm_callback, market_controller, minter +): with boa.env.anchor(): alice = accounts[0] boa.env.time_travel(seconds=WEEK) alice_staked = 0 integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp + checkpoint = boa.env.timestamp checkpoint_rate = crv.rate() checkpoint_supply = 0 checkpoint_balance = 0 - collateral_token._mint_for_testing(alice, 1000 * 10**18, sender=admin) + boa.deal(collateral_token, alice, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply + + t1 = boa.env.timestamp rate1 = crv.rate() t_epoch = crv.start_epoch_time_write(sender=admin) if checkpoint >= t_epoch: rate_x_time = (t1 - checkpoint) * rate1 else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 + rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + ( + t1 - t_epoch + ) * rate1 if checkpoint_supply > 0: integral += rate_x_time * checkpoint_balance // checkpoint_supply checkpoint_rate = rate1 @@ -55,8 +64,12 @@ def update_integral(): else: repay_amount = int(debt * random() * 0.99) market_controller.repay(repay_amount) - min_collateral_required = market_controller.min_collateral(debt - repay_amount, 10) - remove_amount = min(collateral_in_amm - min_collateral_required, amount) + min_collateral_required = market_controller.min_collateral( + debt - repay_amount, 10 + ) + remove_amount = min( + collateral_in_amm - min_collateral_required, amount + ) remove_amount = max(remove_amount, 0) if remove_amount > 0: market_controller.remove_collateral(remove_amount) @@ -66,9 +79,13 @@ def update_integral(): amount = collateral_token.balanceOf(alice) // 5 collateral_token.approve(market_controller.address, amount) if market_controller.loan_exists(alice): - market_controller.borrow_more(amount, int(amount * random() * 2000)) + market_controller.borrow_more( + amount, int(amount * random() * 2000) + ) else: - market_controller.create_loan(amount, int(amount * random() * 2000), 10) + market_controller.create_loan( + amount, int(amount * random() * 2000), 10 + ) update_integral() alice_staked += amount @@ -82,19 +99,21 @@ def update_integral(): update_integral() print(i, dt / 86400, integral, lm_callback.integrate_fraction(alice)) crv_reward = lm_callback.integrate_fraction(alice) - assert approx(crv_reward, integral, 1e-14) + assert crv_reward == pytest.approx(integral, rel=1e-14) minter.mint(lm_callback.address, sender=alice) assert crv.balanceOf(alice) == crv_reward -def test_gauge_integral(accounts, admin, collateral_token, crv, lm_callback, market_controller, minter): +def test_gauge_integral( + accounts, admin, collateral_token, crv, lm_callback, market_controller, minter +): with boa.env.anchor(): alice, bob = accounts[:2] alice_staked = 0 bob_staked = 0 integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp + checkpoint = boa.env.timestamp boa.env.time_travel(blocks=1) checkpoint_rate = crv.rate() checkpoint_supply = 0 @@ -102,19 +121,26 @@ def test_gauge_integral(accounts, admin, collateral_token, crv, lm_callback, mar # Let Alice and Bob have about the same collateral token amount with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) + boa.deal(collateral_token, alice, 1000 * 10**18) + boa.deal(collateral_token, bob, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply + + t1 = boa.env.timestamp rate1 = crv.rate() t_epoch = crv.start_epoch_time() if checkpoint >= t_epoch: rate_x_time = (t1 - checkpoint) * rate1 else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 + rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + ( + t1 - t_epoch + ) * rate1 if checkpoint_supply > 0: integral += rate_x_time * checkpoint_balance // checkpoint_supply checkpoint_rate = rate1 @@ -134,7 +160,9 @@ def update_integral(): is_withdraw_bob = (i > 0) * (random() < 0.5) print("Bob", "withdraws" if is_withdraw_bob else "deposits") if is_withdraw_bob: - collateral_in_amm_bob, _, debt_bob, __ = market_controller.user_state(bob) + collateral_in_amm_bob, _, debt_bob, __ = ( + market_controller.user_state(bob) + ) collateral_bob = lm_callback.user_collateral(bob) assert collateral_in_amm_bob == collateral_bob amount_bob = randrange(1, collateral_in_amm_bob + 1) @@ -144,8 +172,13 @@ def update_integral(): else: repay_amount_bob = int(debt_bob * random() * 0.99) market_controller.repay(repay_amount_bob) - min_collateral_required_bob = market_controller.min_collateral(debt_bob - repay_amount_bob, 10) - remove_amount_bob = min(collateral_in_amm_bob - min_collateral_required_bob, amount_bob) + min_collateral_required_bob = market_controller.min_collateral( + debt_bob - repay_amount_bob, 10 + ) + remove_amount_bob = min( + collateral_in_amm_bob - min_collateral_required_bob, + amount_bob, + ) remove_amount_bob = max(remove_amount_bob, 0) if remove_amount_bob > 0: market_controller.remove_collateral(remove_amount_bob) @@ -155,16 +188,22 @@ def update_integral(): amount_bob = randrange(1, collateral_token.balanceOf(bob) // 10 + 1) collateral_token.approve(market_controller.address, amount_bob) if market_controller.loan_exists(bob): - market_controller.borrow_more(amount_bob, int(amount_bob * random() * 2000)) + market_controller.borrow_more( + amount_bob, int(amount_bob * random() * 2000) + ) else: - market_controller.create_loan(amount_bob, int(amount_bob * random() * 2000), 10) + market_controller.create_loan( + amount_bob, int(amount_bob * random() * 2000), 10 + ) update_integral() bob_staked += amount_bob if is_alice: # For Alice with boa.env.prank(alice): - collateral_in_amm_alice, _, debt_alice, __ = market_controller.user_state(alice) + collateral_in_amm_alice, _, debt_alice, __ = ( + market_controller.user_state(alice) + ) collateral_alice = lm_callback.user_collateral(alice) assert collateral_in_amm_alice == collateral_alice is_withdraw_alice = (collateral_in_amm_alice > 0) * (random() < 0.5) @@ -178,20 +217,35 @@ def update_integral(): else: repay_amount_alice = int(debt_alice * random() * 0.99) market_controller.repay(repay_amount_alice) - min_collateral_required_alice = market_controller.min_collateral(debt_alice - repay_amount_alice, 10) - remove_amount_alice = min(collateral_in_amm_alice - min_collateral_required_alice, amount_alice) + min_collateral_required_alice = ( + market_controller.min_collateral( + debt_alice - repay_amount_alice, 10 + ) + ) + remove_amount_alice = min( + collateral_in_amm_alice - min_collateral_required_alice, + amount_alice, + ) remove_amount_alice = max(remove_amount_alice, 0) if remove_amount_alice > 0: market_controller.remove_collateral(remove_amount_alice) update_integral() alice_staked -= remove_amount_alice else: - amount_alice = randrange(1, collateral_token.balanceOf(alice) // 10 + 1) - collateral_token.approve(market_controller.address, amount_alice) + amount_alice = randrange( + 1, collateral_token.balanceOf(alice) // 10 + 1 + ) + collateral_token.approve( + market_controller.address, amount_alice + ) if market_controller.loan_exists(alice): - market_controller.borrow_more(amount_alice, int(amount_alice * random() * 2000)) + market_controller.borrow_more( + amount_alice, int(amount_alice * random() * 2000) + ) else: - market_controller.create_loan(amount_alice, int(amount_alice * random() * 2000), 10) + market_controller.create_loan( + amount_alice, int(amount_alice * random() * 2000), 10 + ) update_integral() alice_staked += amount_alice @@ -218,7 +272,9 @@ def update_integral(): update_integral() print(i, dt / 86400, integral, lm_callback.integrate_fraction(alice)) - assert approx(lm_callback.integrate_fraction(alice), integral, 1e-14) + assert lm_callback.integrate_fraction(alice) == pytest.approx( + integral, rel=1e-14 + ) with boa.env.prank(bob): crv_balance = crv.balanceOf(bob) @@ -229,19 +285,19 @@ def update_integral(): def test_set_killed( - accounts, - admin, - collateral_token, - crv, - market_controller, - lm_callback, - minter, + accounts, + admin, + collateral_token, + crv, + market_controller, + lm_callback, + minter, ): alice = accounts[0] boa.env.time_travel(seconds=2 * WEEK + 5) with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) + boa.deal(collateral_token, alice, 1000 * 10**18) # Alice creates loan market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) @@ -258,7 +314,7 @@ def test_set_killed( assert crv.balanceOf(alice) - crv_balance == rewards_alice # Kill lm callback - with boa.reverts('only owner'): + with boa.reverts("only owner"): lm_callback.set_killed(True, sender=alice) lm_callback.set_killed(True, sender=admin) @@ -274,7 +330,7 @@ def test_set_killed( assert crv.balanceOf(alice) == crv_balance # Unkill lm callback - with boa.reverts('only owner'): + with boa.reverts("only owner"): lm_callback.set_killed(False, sender=alice) lm_callback.set_killed(False, sender=admin) diff --git a/tests/lm_callback/test_lm_callback.py b/tests/lm_callback/test_lm_callback.py index e3d4599b..273855cf 100644 --- a/tests/lm_callback/test_lm_callback.py +++ b/tests/lm_callback/test_lm_callback.py @@ -1,30 +1,30 @@ import boa +import pytest from random import random, randrange, choice -from ..conftest import approx -MAX_UINT256 = 2 ** 256 - 1 +MAX_UINT256 = 2**256 - 1 YEAR = 365 * 86400 WEEK = 7 * 86400 def test_simple_exchange( - accounts, - admin, - chad, - collateral_token, - crv, - market_controller, - market_amm, - lm_callback, - minter, + accounts, + admin, + chad, + collateral_token, + crv, + market_controller, + market_amm, + lm_callback, + minter, ): alice, bob = accounts[:2] boa.env.time_travel(seconds=2 * WEEK + 5) # Let Alice and Bob have about the same collateral token amount with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) - collateral_token._mint_for_testing(bob, 1000 * 10 ** 18) + boa.deal(collateral_token, alice, 1000 * 10**18) + boa.deal(collateral_token, bob, 1000 * 10**18) # Alice and Bob create loan market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) @@ -54,7 +54,7 @@ def test_simple_exchange( rewards_bob = lm_callback.integrate_fraction(bob) d_alice = rewards_alice - old_rewards_alice d_bob = rewards_bob - old_rewards_bob - assert approx(d_bob / d_alice, 2, 1e-15) + assert d_bob / d_alice == pytest.approx(2, rel=1e-15) minter.mint(lm_callback.address, sender=alice) assert crv.balanceOf(alice) == rewards_alice @@ -64,22 +64,22 @@ def test_simple_exchange( def test_gauge_integral_with_exchanges( - accounts, - admin, - chad, - collateral_token, - crv, - lm_callback, - market_controller, - market_amm, - price_oracle, - minter, + accounts, + admin, + chad, + collateral_token, + crv, + lm_callback, + market_controller, + market_amm, + price_oracle, + minter, ): with boa.env.anchor(): alice, bob = accounts[:2] integral = 0 # ∫(balance * rate(t) / totalSupply(t) dt) - checkpoint = boa.env.evm.patch.timestamp + checkpoint = boa.env.timestamp checkpoint_rate = crv.rate() checkpoint_supply = 0 checkpoint_balance = 0 @@ -88,24 +88,31 @@ def test_gauge_integral_with_exchanges( # Let Alice and Bob have about the same collateral token amount with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) + boa.deal(collateral_token, alice, 1000 * 10**18) + boa.deal(collateral_token, bob, 1000 * 10**18) def update_integral(): - nonlocal checkpoint, checkpoint_rate, integral, checkpoint_balance, checkpoint_supply - - t1 = boa.env.evm.patch.timestamp + nonlocal \ + checkpoint, \ + checkpoint_rate, \ + integral, \ + checkpoint_balance, \ + checkpoint_supply + + t1 = boa.env.timestamp t_epoch = crv.start_epoch_time_write(sender=admin) rate1 = crv.rate() if checkpoint >= t_epoch: rate_x_time = (t1 - checkpoint) * rate1 else: - rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + (t1 - t_epoch) * rate1 + rate_x_time = (t_epoch - checkpoint) * checkpoint_rate + ( + t1 - t_epoch + ) * rate1 if checkpoint_supply > 0: integral += rate_x_time * checkpoint_balance // checkpoint_supply checkpoint_rate = rate1 checkpoint = t1 - checkpoint_supply = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() + checkpoint_supply = collateral_token.balanceOf(market_amm) checkpoint_balance = market_amm.get_sum_xy(alice)[1] # Now let's have a loop where Bob always deposit or withdraws, @@ -118,7 +125,9 @@ def update_integral(): # For Bob with boa.env.prank(bob): - collateral_in_amm_bob, stablecoin_in_amm_bob, debt_bob, __ = market_controller.user_state(bob) + collateral_in_amm_bob, stablecoin_in_amm_bob, debt_bob, __ = ( + market_controller.user_state(bob) + ) is_withdraw_bob = (collateral_in_amm_bob > 0) * (random() < 0.5) is_underwater_bob = stablecoin_in_amm_bob > 0 @@ -128,38 +137,62 @@ def update_integral(): print("Bob repays (full):", debt_bob) print("Bob withdraws (full):", amount_bob) market_controller.repay(debt_bob) - assert approx(market_amm.get_sum_xy(bob)[1], lm_callback.user_collateral(bob), 1e-13) + assert market_amm.get_sum_xy(bob)[1] == pytest.approx( + lm_callback.user_collateral(bob), rel=1e-13 + ) elif market_controller.health(bob) > 0: - repay_amount_bob = int(debt_bob // 10 + (debt_bob * 9 // 10) * random() * 0.99) + repay_amount_bob = int( + debt_bob // 10 + (debt_bob * 9 // 10) * random() * 0.99 + ) print("Bob repays:", repay_amount_bob) market_controller.repay(repay_amount_bob) if not is_underwater_bob: - min_collateral_required_bob = market_controller.min_collateral(debt_bob - repay_amount_bob, 10) - remove_amount_bob = min(collateral_in_amm_bob - min_collateral_required_bob, amount_bob) + min_collateral_required_bob = ( + market_controller.min_collateral( + debt_bob - repay_amount_bob, 10 + ) + ) + remove_amount_bob = min( + collateral_in_amm_bob - min_collateral_required_bob, + amount_bob, + ) remove_amount_bob = max(remove_amount_bob, 0) if remove_amount_bob > 0: print("Bob withdraws:", remove_amount_bob) market_controller.remove_collateral(remove_amount_bob) - assert approx(market_amm.get_sum_xy(bob)[1], lm_callback.user_collateral(bob), 1e-13) + assert market_amm.get_sum_xy(bob)[1] == pytest.approx( + lm_callback.user_collateral(bob), rel=1e-13 + ) update_integral() elif not is_underwater_bob: amount_bob = randrange(1, collateral_token.balanceOf(bob) // 10 + 1) collateral_token.approve(market_controller.address, amount_bob) - max_borrowable_bob = market_controller.max_borrowable(amount_bob + collateral_in_amm_bob, 10, debt_bob) - borrow_amount_bob = min(int(random() * (max_borrowable_bob - debt_bob)), max_borrowable_bob - debt_bob) + max_borrowable_bob = market_controller.max_borrowable( + amount_bob + collateral_in_amm_bob, 10, debt_bob + ) + borrow_amount_bob = min( + int(random() * (max_borrowable_bob - debt_bob)), + max_borrowable_bob - debt_bob, + ) if borrow_amount_bob > 0: print("Bob deposits:", amount_bob, borrow_amount_bob) if market_controller.loan_exists(bob): market_controller.borrow_more(amount_bob, borrow_amount_bob) else: - market_controller.create_loan(amount_bob, borrow_amount_bob, 10) + market_controller.create_loan( + amount_bob, borrow_amount_bob, 10 + ) update_integral() - assert approx(market_amm.get_sum_xy(bob)[1], lm_callback.user_collateral(bob), 1e-13) + assert market_amm.get_sum_xy(bob)[1] == pytest.approx( + lm_callback.user_collateral(bob), rel=1e-13 + ) # For Alice if is_alice: with boa.env.prank(alice): - collateral_in_amm_alice, stablecoin_in_amm_alice, debt_alice, __ = market_controller.user_state(alice) + collateral_in_amm_alice, stablecoin_in_amm_alice, debt_alice, __ = ( + market_controller.user_state(alice) + ) is_withdraw_alice = (collateral_in_amm_alice > 0) * (random() < 0.5) is_underwater_alice = stablecoin_in_amm_alice > 0 @@ -169,46 +202,99 @@ def update_integral(): print("Alice repays (full):", debt_alice) print("Alice withdraws (full):", amount_alice) market_controller.repay(debt_alice) - assert approx(market_amm.get_sum_xy(alice)[1], lm_callback.user_collateral(alice), 1e-13) + assert market_amm.get_sum_xy(alice)[1] == pytest.approx( + lm_callback.user_collateral(alice), rel=1e-13 + ) elif market_controller.health(alice) > 0: - repay_amount_alice = int(debt_alice // 10 + (debt_alice * 9 // 10) * random() * 0.99) + repay_amount_alice = int( + debt_alice // 10 + + (debt_alice * 9 // 10) * random() * 0.99 + ) print("Alice repays:", repay_amount_alice) market_controller.repay(repay_amount_alice) if not is_underwater_alice: - min_collateral_required_alice = market_controller.min_collateral(debt_alice - repay_amount_alice, 10) - remove_amount_alice = min(collateral_in_amm_alice - min_collateral_required_alice, amount_alice) + min_collateral_required_alice = ( + market_controller.min_collateral( + debt_alice - repay_amount_alice, 10 + ) + ) + remove_amount_alice = min( + collateral_in_amm_alice + - min_collateral_required_alice, + amount_alice, + ) remove_amount_alice = max(remove_amount_alice, 0) if remove_amount_alice > 0: print("Alice withdraws:", remove_amount_alice) - market_controller.remove_collateral(remove_amount_alice) - assert approx(market_amm.get_sum_xy(alice)[1], lm_callback.user_collateral(alice), 1e-13) + market_controller.remove_collateral( + remove_amount_alice + ) + assert market_amm.get_sum_xy(alice)[1] == pytest.approx( + lm_callback.user_collateral(alice), rel=1e-13 + ) update_integral() elif not is_underwater_alice: - amount_alice = randrange(1, collateral_token.balanceOf(alice) // 10 + 1) - collateral_token.approve(market_controller.address, amount_alice) - max_borrowable_alice = market_controller.max_borrowable(amount_alice + collateral_in_amm_alice, 10, debt_alice) - borrow_amount_alice = min(int(random() * (max_borrowable_alice - debt_alice)), max_borrowable_alice - debt_alice) + amount_alice = randrange( + 1, collateral_token.balanceOf(alice) // 10 + 1 + ) + collateral_token.approve( + market_controller.address, amount_alice + ) + max_borrowable_alice = market_controller.max_borrowable( + amount_alice + collateral_in_amm_alice, 10, debt_alice + ) + borrow_amount_alice = min( + int(random() * (max_borrowable_alice - debt_alice)), + max_borrowable_alice - debt_alice, + ) if borrow_amount_alice > 0: print("Alice deposits:", amount_alice, borrow_amount_alice) if market_controller.loan_exists(alice): - market_controller.borrow_more(amount_alice, borrow_amount_alice) + market_controller.borrow_more( + amount_alice, borrow_amount_alice + ) else: - market_controller.create_loan(amount_alice, borrow_amount_alice, 10) + market_controller.create_loan( + amount_alice, borrow_amount_alice, 10 + ) update_integral() - assert approx(market_amm.get_sum_xy(alice)[1], lm_callback.user_collateral(alice), 1e-13) + assert market_amm.get_sum_xy(alice)[1] == pytest.approx( + lm_callback.user_collateral(alice), rel=1e-13 + ) # Chad trading alice_bands = market_amm.read_user_tick_numbers(alice) - alice_bands = [] if alice_bands[0] == alice_bands[1] else list(range(alice_bands[0], alice_bands[1] + 1)) + alice_bands = ( + [] + if alice_bands[0] == alice_bands[1] + else list(range(alice_bands[0], alice_bands[1] + 1)) + ) bob_bands = market_amm.read_user_tick_numbers(bob) - bob_bands = [] if bob_bands[0] == bob_bands[1] else list(range(bob_bands[0], bob_bands[1] + 1)) + bob_bands = ( + [] + if bob_bands[0] == bob_bands[1] + else list(range(bob_bands[0], bob_bands[1] + 1)) + ) available_bands = alice_bands + bob_bands print("Bob bands:", bob_bands) print("Alice bands:", alice_bands) print("Active band:", market_amm.active_band()) p_o = market_amm.price_oracle() - upper_bands = sorted(list(filter(lambda band: market_amm.p_oracle_down(band) > p_o, available_bands)))[-5:] - lower_bands = sorted(list(filter(lambda band: market_amm.p_oracle_up(band) < p_o, available_bands)))[:5] + upper_bands = sorted( + list( + filter( + lambda band: market_amm.p_oracle_down(band) > p_o, + available_bands, + ) + ) + )[-5:] + lower_bands = sorted( + list( + filter( + lambda band: market_amm.p_oracle_up(band) < p_o, available_bands + ) + ) + )[:5] available_bands = upper_bands + lower_bands if len(available_bands) > 0: target_band = choice(available_bands) @@ -238,11 +324,17 @@ def update_integral(): boa.env.time_travel(seconds=dt) print("Time travel", dt) - total_collateral_from_amm = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() + total_collateral_from_amm = collateral_token.balanceOf(market_amm) total_collateral_from_lm_cb = lm_callback.total_collateral() - print("Total collateral:", total_collateral_from_amm, total_collateral_from_lm_cb) + print( + "Total collateral:", + total_collateral_from_amm, + total_collateral_from_lm_cb, + ) if total_collateral_from_amm > 0 and total_collateral_from_lm_cb > 0: - assert approx(total_collateral_from_amm, total_collateral_from_lm_cb, 1e-13) + assert total_collateral_from_amm == pytest.approx( + total_collateral_from_lm_cb, rel=1e-13 + ) with boa.env.prank(alice): crv_balance = crv.balanceOf(alice) @@ -253,7 +345,9 @@ def update_integral(): update_integral() print(i, dt / 86400, integral, lm_callback.integrate_fraction(alice)) - assert approx(lm_callback.integrate_fraction(alice), integral, 1e-14) + assert lm_callback.integrate_fraction(alice) == pytest.approx( + integral, rel=1e-14 + ) with boa.env.prank(bob): crv_balance = crv.balanceOf(bob) @@ -264,24 +358,24 @@ def update_integral(): def test_full_repay_underwater( - accounts, - admin, - chad, - collateral_token, - crv, - lm_callback, - market_controller, - market_amm, - price_oracle, - minter, + accounts, + admin, + chad, + collateral_token, + crv, + lm_callback, + market_controller, + market_amm, + price_oracle, + minter, ): with boa.env.anchor(): alice, bob = accounts[:2] # Let Alice and Bob have about the same collateral token amount with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10**18) - collateral_token._mint_for_testing(bob, 1000 * 10**18) + boa.deal(collateral_token, alice, 1000 * 10**18) + boa.deal(collateral_token, bob, 1000 * 10**18) dt = randrange(1, YEAR // 5) boa.env.time_travel(seconds=dt) @@ -300,8 +394,7 @@ def test_full_repay_underwater( market_controller.create_loan(amount_alice, int(amount_alice * 500), 10) print("Alice deposits:", amount_alice) - print(collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y(), - lm_callback.total_collateral()) + print(collateral_token.balanceOf(market_amm), lm_callback.total_collateral()) dt = randrange(1, YEAR // 5) boa.env.time_travel(seconds=dt) @@ -332,10 +425,14 @@ def test_full_repay_underwater( print("Bob repays (full):", debt_bob) print("Bob withdraws (full):", amount_bob) - total_collateral_from_amm = collateral_token.balanceOf(market_amm) - market_amm.admin_fees_y() + total_collateral_from_amm = collateral_token.balanceOf(market_amm) total_collateral_from_lm_cb = lm_callback.total_collateral() - print("Total collateral:", total_collateral_from_amm, total_collateral_from_lm_cb) - assert approx(total_collateral_from_amm, total_collateral_from_lm_cb, 1e-15) + print( + "Total collateral:", total_collateral_from_amm, total_collateral_from_lm_cb + ) + assert total_collateral_from_amm == pytest.approx( + total_collateral_from_lm_cb, rel=1e-15 + ) for user in accounts[:2]: with boa.env.prank(user): diff --git a/tests/lm_callback/test_lm_callback_reverts.py b/tests/lm_callback/test_lm_callback_reverts.py index 26ed10a8..99b9d7ea 100644 --- a/tests/lm_callback/test_lm_callback_reverts.py +++ b/tests/lm_callback/test_lm_callback_reverts.py @@ -1,26 +1,27 @@ import boa +from tests.utils.deployers import LM_CALLBACK_WITH_REVERTS_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS WEEK = 7 * 86400 def test_add_new_lm_callback( - accounts, - admin, - chad, - collateral_token, - stablecoin, - market_controller, - market_amm, - gauge_controller, + accounts, + admin, + chad, + collateral_token, + stablecoin, + market_controller, + market_amm, + gauge_controller, ): alice = accounts[0] # Remove current LM Callback - market_controller.set_callback("0x0000000000000000000000000000000000000000", sender=admin) + market_controller.set_callback(ZERO_ADDRESS, sender=admin) boa.env.time_travel(seconds=2 * WEEK + 5) - collateral_token._mint_for_testing(alice, 10**22, sender=admin) - + boa.deal(collateral_token, alice, 10**22) alice_balances0 = [stablecoin.balanceOf(alice), collateral_token.balanceOf(alice)] chad_balances0 = [stablecoin.balanceOf(chad), collateral_token.balanceOf(chad)] @@ -39,9 +40,9 @@ def test_add_new_lm_callback( # Wire up the new LM Callback reverting on any AMM interaction with boa.env.prank(admin): - new_cb = boa.load('contracts/testing/LMCallbackWithReverts.vy') + new_cb = LM_CALLBACK_WITH_REVERTS_DEPLOYER.deploy() market_controller.set_callback(new_cb) - gauge_controller.add_gauge(new_cb.address, 0, 10 ** 18) + gauge_controller.add_gauge(new_cb.address, 0, 10**18) # Market interactions are still working market_amm.exchange(1, 0, 10**20, 0, sender=chad) @@ -50,7 +51,7 @@ def test_add_new_lm_callback( alice_balances2 = [stablecoin.balanceOf(alice), collateral_token.balanceOf(alice)] chad_balances2 = [stablecoin.balanceOf(chad), collateral_token.balanceOf(chad)] - assert alice_balances2[0] - alice_balances1[0] == 10 ** 20 - assert alice_balances1[1] - alice_balances2[1] == 10 ** 17 + assert alice_balances2[0] - alice_balances1[0] == 10**20 + assert alice_balances1[1] - alice_balances2[1] == 10**17 assert chad_balances1[0] < chad_balances2[0] assert chad_balances1[1] > chad_balances2[1] diff --git a/tests/lm_callback/test_rewards_kill_unkill.py b/tests/lm_callback/test_rewards_kill_unkill.py index e7a2464e..357d87b0 100644 --- a/tests/lm_callback/test_rewards_kill_unkill.py +++ b/tests/lm_callback/test_rewards_kill_unkill.py @@ -4,15 +4,15 @@ def test_rewards_kill( - accounts, - admin, - chad, - collateral_token, - crv, - market_controller, - market_amm, - lm_callback, - minter, + accounts, + admin, + chad, + collateral_token, + crv, + market_controller, + market_amm, + lm_callback, + minter, ): print("") alice = accounts[0] @@ -20,7 +20,7 @@ def test_rewards_kill( boa.env.time_travel(seconds=2 * WEEK + 5) with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) + boa.deal(collateral_token, alice, 1000 * 10**18) market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) @@ -53,15 +53,15 @@ def test_rewards_kill( def test_rewards_kill_unkill( - accounts, - admin, - chad, - collateral_token, - crv, - market_controller, - market_amm, - lm_callback, - minter, + accounts, + admin, + chad, + collateral_token, + crv, + market_controller, + market_amm, + lm_callback, + minter, ): print("") alice = accounts[0] @@ -69,7 +69,7 @@ def test_rewards_kill_unkill( boa.env.time_travel(seconds=2 * WEEK + 5) with boa.env.prank(admin): - collateral_token._mint_for_testing(alice, 1000 * 10 ** 18) + boa.deal(collateral_token, alice, 1000 * 10**18) market_controller.create_loan(10**21, 10**21 * 2600, 10, sender=alice) @@ -103,7 +103,10 @@ def test_rewards_kill_unkill( lm_callback.user_checkpoint(alice, sender=alice) rewards1 = lm_callback.integrate_fraction(alice) - print(rewards1, "- Rewards WITH user_checkpoint call before killing and WITH gauge calls between kill-unkill") + print( + rewards1, + "- Rewards WITH user_checkpoint call before killing and WITH gauge calls between kill-unkill", + ) with boa.env.anchor(): boa.env.time_travel(WEEK) @@ -121,11 +124,14 @@ def test_rewards_kill_unkill( lm_callback.user_checkpoint(alice, sender=alice) rewards2 = lm_callback.integrate_fraction(alice) - print(rewards2, "- Rewards WITH user_checkpoint call before killing and WITHOUT gauge calls between kill-unkill") + print( + rewards2, + "- Rewards WITH user_checkpoint call before killing and WITHOUT gauge calls between kill-unkill", + ) with boa.env.anchor(): boa.env.time_travel(WEEK) - #lm_callback.user_checkpoint(alice, sender=alice) + # lm_callback.user_checkpoint(alice, sender=alice) with boa.env.prank(admin): lm_callback.set_killed(True) @@ -139,7 +145,10 @@ def test_rewards_kill_unkill( lm_callback.user_checkpoint(alice, sender=alice) rewards3 = lm_callback.integrate_fraction(alice) - print(rewards3, "- Rewards WITHOUT user_checkpoint call before killing and WITH gauge calls between kill-unkill") + print( + rewards3, + "- Rewards WITHOUT user_checkpoint call before killing and WITH gauge calls between kill-unkill", + ) with boa.env.anchor(): boa.env.time_travel(WEEK) @@ -157,7 +166,17 @@ def test_rewards_kill_unkill( lm_callback.user_checkpoint(alice, sender=alice) rewards4 = lm_callback.integrate_fraction(alice) - print(rewards4, "- Rewards WITHOUT user_checkpoint call before killing and WITHOUT gauge calls between kill-unkill") + print( + rewards4, + "- Rewards WITHOUT user_checkpoint call before killing and WITHOUT gauge calls between kill-unkill", + ) # Checkpoints cause little inaccuracy - assert rewards1 == rewards2 == rewards3 == rewards4 - 10**6 == rewards_ref - 10**6 == 3 * rewards0 + assert ( + rewards1 + == rewards2 + == rewards3 + == rewards4 - 10**6 + == rewards_ref - 10**6 + == 3 * rewards0 + ) diff --git a/tests/lm_callback/test_st_as_gauge.py b/tests/lm_callback/test_st_as_gauge.py index 85c0d008..724e34ca 100644 --- a/tests/lm_callback/test_st_as_gauge.py +++ b/tests/lm_callback/test_st_as_gauge.py @@ -1,14 +1,19 @@ import boa from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) from random import random -from ..conftest import approx +import pytest class StateMachine(RuleBasedStateMachine): user_id = st.integers(min_value=0, max_value=4) - value = st.integers(min_value=10**16, max_value=10 ** 18 * 10 ** 6 // 3000) + value = st.integers(min_value=10**16, max_value=10**18 * 10**6 // 3000) time = st.integers(min_value=300, max_value=86400 * 90) lock_time = st.integers(min_value=86400 * 7, max_value=86400 * 365 * 4) @@ -16,15 +21,18 @@ def __init__(self): super().__init__() self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, - "integral": 0, - "collateral": 0, - } for addr in self.accounts[:5]} + self.integrals = { + addr: { + "checkpoint": boa.env.timestamp, + "integral": 0, + "collateral": 0, + } + for addr in self.accounts[:5] + } def update_integrals(self, user, d_balance=0): # Update rewards - t1 = boa.env.evm.patch.timestamp + t1 = boa.env.timestamp t_epoch = self.crv.start_epoch_time_write(sender=self.admin) rate1 = self.crv.rate() for acct in self.accounts[:5]: @@ -32,9 +40,15 @@ def update_integrals(self, user, d_balance=0): if integral["checkpoint"] >= t_epoch: rate_x_time = (t1 - integral["checkpoint"]) * rate1 else: - rate_x_time = (t_epoch - integral["checkpoint"]) * self.checkpoint_rate + (t1 - t_epoch) * rate1 + rate_x_time = ( + t_epoch - integral["checkpoint"] + ) * self.checkpoint_rate + (t1 - t_epoch) * rate1 if self.checkpoint_total_collateral > 0: - integral["integral"] += rate_x_time * integral["collateral"] // self.checkpoint_total_collateral + integral["integral"] += ( + rate_x_time + * integral["collateral"] + // self.checkpoint_total_collateral + ) integral["checkpoint"] = t1 if acct == user: integral["collateral"] += d_balance @@ -56,14 +70,23 @@ def deposit(self, uid, value): if value > 0: if self.market_controller.loan_exists(user): - self.market_controller.borrow_more(value, int(value * random() * 2000)) + self.market_controller.borrow_more( + value, int(value * random() * 2000) + ) else: - self.market_controller.create_loan(value, int(value * random() * 2000), 10) + self.market_controller.create_loan( + value, int(value * random() * 2000), 10 + ) self.update_integrals(user, value) assert self.collateral_token.balanceOf(user) == balance - value - if self.integrals[user]["integral"] > 0 and self.lm_callback.integrate_fraction(user) > 0: - assert approx(self.lm_callback.integrate_fraction(user), self.integrals[user]["integral"], 1e-13) + if ( + self.integrals[user]["integral"] > 0 + and self.lm_callback.integrate_fraction(user) > 0 + ): + assert self.lm_callback.integrate_fraction(user) == pytest.approx( + self.integrals[user]["integral"], rel=1e-13 + ) @rule(uid=user_id, value=value) def withdraw(self, uid, value): @@ -83,7 +106,9 @@ def withdraw(self, uid, value): else: repay_amount = int(debt * random() * 0.99) self.market_controller.repay(repay_amount) - min_collateral_required = self.market_controller.min_collateral(debt - repay_amount, 10) + min_collateral_required = self.market_controller.min_collateral( + debt - repay_amount, 10 + ) remove_amount = min(collateral_in_amm - min_collateral_required, value) remove_amount = max(remove_amount, 0) if remove_amount > 0: @@ -91,8 +116,13 @@ def withdraw(self, uid, value): self.update_integrals(user, -remove_amount) assert self.collateral_token.balanceOf(user) == balance + remove_amount - if self.integrals[user]["integral"] > 0 and self.lm_callback.integrate_fraction(user) > 0: - assert approx(self.lm_callback.integrate_fraction(user), self.integrals[user]["integral"], 1e-13) + if ( + self.integrals[user]["integral"] > 0 + and self.lm_callback.integrate_fraction(user) > 0 + ): + assert self.lm_callback.integrate_fraction(user) == pytest.approx( + self.integrals[user]["integral"], rel=1e-13 + ) @rule(dt=time) def advance_time(self, dt): @@ -114,7 +144,7 @@ def checkpoint(self, uid): r2 = self.integrals[user]["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) + assert r1 == pytest.approx(r2, rel=1e-13) @rule(uid=user_id) def claim_crv(self, uid): @@ -137,7 +167,9 @@ def invariant_collateral(self): """ for account, integral in self.integrals.items(): assert self.lm_callback.user_collateral(account) == integral["collateral"] - assert self.lm_callback.total_collateral() == sum([i["collateral"] for i in self.integrals.values()]) + assert self.lm_callback.total_collateral() == sum( + [i["collateral"] for i in self.integrals.values()] + ) def teardown(self): """ @@ -148,17 +180,21 @@ def teardown(self): initial_collateral = self.collateral_token.balanceOf(account) collateral_in_amm = integral["collateral"] debt = self.market_controller.user_state(account)[2] - self.market_controller.repay(debt) + if debt > 0: + self.market_controller.repay(debt) self.update_integrals(account) assert not self.market_controller.loan_exists(account) - assert self.collateral_token.balanceOf(account) == initial_collateral + collateral_in_amm + assert ( + self.collateral_token.balanceOf(account) + == initial_collateral + collateral_in_amm + ) r1 = self.lm_callback.integrate_fraction(account) r2 = integral["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) + assert r1 == pytest.approx(r2, rel=1e-13) crv_balance = self.crv.balanceOf(account) with boa.env.anchor(): @@ -168,21 +204,21 @@ def teardown(self): def test_state_machine( - accounts, - admin, - collateral_token, - crv, - lm_callback, - market_controller, - minter, + accounts, + admin, + collateral_token, + crv, + lm_callback, + market_controller, + minter, ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) + boa.deal(collateral_token, acct, 1000 * 10**18) + crv.transfer(acct, 10**20) with boa.env.prank(acct): - collateral_token.approve(market_controller, 2 ** 256 - 1) + collateral_token.approve(market_controller, 2**256 - 1) boa.env.time_travel(seconds=7 * 86400) diff --git a/tests/lm_callback/test_st_lm_callback.py b/tests/lm_callback/test_st_lm_callback.py index 8db32670..2c1d69fd 100644 --- a/tests/lm_callback/test_st_lm_callback.py +++ b/tests/lm_callback/test_st_lm_callback.py @@ -1,8 +1,13 @@ import boa from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant -from ..conftest import approx +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) +import pytest class StateMachine(RuleBasedStateMachine): @@ -20,15 +25,18 @@ def __init__(self): super().__init__() self.checkpoint_total_collateral = 0 self.checkpoint_rate = self.crv.rate() - self.integrals = {addr: { - "checkpoint": boa.env.evm.patch.timestamp, - "integral": 0, - "collateral": 0, - } for addr in self.accounts[:5]} + self.integrals = { + addr: { + "checkpoint": boa.env.timestamp, + "integral": 0, + "collateral": 0, + } + for addr in self.accounts[:5] + } def update_integrals(self): # Update rewards - t1 = boa.env.evm.patch.timestamp + t1 = boa.env.timestamp t_epoch = self.crv.start_epoch_time_write(sender=self.admin) rate1 = self.crv.rate() for acct in self.accounts[:5]: @@ -36,12 +44,20 @@ def update_integrals(self): if integral["checkpoint"] >= t_epoch: rate_x_time = (t1 - integral["checkpoint"]) * rate1 else: - rate_x_time = (t_epoch - integral["checkpoint"]) * self.checkpoint_rate + (t1 - t_epoch) * rate1 + rate_x_time = ( + t_epoch - integral["checkpoint"] + ) * self.checkpoint_rate + (t1 - t_epoch) * rate1 if self.checkpoint_total_collateral > 0: - integral["integral"] += rate_x_time * integral["collateral"] // self.checkpoint_total_collateral + integral["integral"] += ( + rate_x_time + * integral["collateral"] + // self.checkpoint_total_collateral + ) integral["checkpoint"] = t1 integral["collateral"] = self.market_amm.get_sum_xy(acct)[1] - self.checkpoint_total_collateral = self.collateral_token.balanceOf(self.market_amm) - self.market_amm.admin_fees_y() + self.checkpoint_total_collateral = self.collateral_token.balanceOf( + self.market_amm + ) self.checkpoint_rate = rate1 @rule(uid=user_id, deposit_pct=deposit_pct, borrow_pct=borrow_pct) @@ -56,13 +72,21 @@ def deposit(self, uid, deposit_pct, borrow_pct): with boa.env.prank(user): balance = self.collateral_token.balanceOf(user) deposit_amount = min(int(balance * deposit_pct), balance) - collateral_in_amm, stablecoin_in_amm, debt, __ = self.market_controller.user_state(user) - max_borrowable = self.market_controller.max_borrowable(deposit_amount + collateral_in_amm, 10, debt) - borrow_amount = min(int((max_borrowable - debt) * borrow_pct), max_borrowable - debt) + collateral_in_amm, stablecoin_in_amm, debt, __ = ( + self.market_controller.user_state(user) + ) + max_borrowable = self.market_controller.max_borrowable( + deposit_amount + collateral_in_amm, 10, debt + ) + borrow_amount = min( + int((max_borrowable - debt) * borrow_pct), max_borrowable - debt + ) i = 1 while True: try: - self.market_controller.calculate_debt_n1(collateral_in_amm + deposit_amount, debt + borrow_amount, 10) + self.market_controller.calculate_debt_n1( + collateral_in_amm + deposit_amount, debt + borrow_amount, 10 + ) break except Exception: if i == 100: @@ -84,7 +108,7 @@ def deposit(self, uid, deposit_pct, borrow_pct): r2 = self.integrals[user]["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 + assert r1 == pytest.approx(r2, rel=1e-13) or abs(r1 - r2) < 100 @rule(uid=user_id, withdraw_pct=withdraw_pct, repay_pct=repay_pct) def withdraw(self, uid, withdraw_pct, repay_pct): @@ -94,7 +118,9 @@ def withdraw(self, uid, withdraw_pct, repay_pct): user = self.accounts[uid] with boa.env.prank(user): balance = self.collateral_token.balanceOf(user) - collateral_in_amm, stablecoin_in_amm, debt, _ = self.market_controller.user_state(user) + collateral_in_amm, stablecoin_in_amm, debt, _ = ( + self.market_controller.user_state(user) + ) is_underwater = stablecoin_in_amm > 0 if collateral_in_amm == 0: return @@ -105,6 +131,9 @@ def withdraw(self, uid, withdraw_pct, repay_pct): withdraw_amount = collateral_in_amm elif self.market_controller.health(user) > 0: repay_amount = int(debt * repay_pct) + if repay_amount == 0: + return + self.market_controller.repay(repay_amount) if is_underwater: # Underwater repay does not trigger callback, so we call checkpoint manually to pass checks below @@ -112,8 +141,14 @@ def withdraw(self, uid, withdraw_pct, repay_pct): else: A = self.market_amm.A() withdraw_amount = int(collateral_in_amm * withdraw_pct) - min_collateral_required = self.market_controller.min_collateral(debt - repay_amount, 10) * A // (A - 1) - withdraw_amount = min(collateral_in_amm - min_collateral_required, withdraw_amount) + min_collateral_required = ( + self.market_controller.min_collateral(debt - repay_amount, 10) + * A + // (A - 1) + ) + withdraw_amount = min( + collateral_in_amm - min_collateral_required, withdraw_amount + ) withdraw_amount = max(withdraw_amount, 0) if withdraw_amount > 0: self.market_controller.remove_collateral(withdraw_amount) @@ -128,7 +163,7 @@ def withdraw(self, uid, withdraw_pct, repay_pct): r2 = self.integrals[user]["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 + assert r1 == pytest.approx(r2, rel=1e-13) or abs(r1 - r2) < 100 @rule(target_band_pct=target_band_pct, target_price_pct=target_price_pct) def trade(self, target_band_pct, target_price_pct): @@ -139,19 +174,43 @@ def trade(self, target_band_pct, target_price_pct): available_bands = [] for acct in self.accounts[:5]: border_bands = self.market_amm.read_user_tick_numbers(acct) - available_bands += [] if border_bands[0] == border_bands[1] else list(range(border_bands[0], border_bands[1] + 1)) + available_bands += ( + [] + if border_bands[0] == border_bands[1] + else list(range(border_bands[0], border_bands[1] + 1)) + ) p_o = self.market_amm.price_oracle() - upper_bands = sorted(list(filter(lambda band: self.market_amm.p_oracle_down(band) > p_o, available_bands)))[-5:] - lower_bands = sorted(list(filter(lambda band: self.market_amm.p_oracle_up(band) < p_o, available_bands)))[:5] + upper_bands = sorted( + list( + filter( + lambda band: self.market_amm.p_oracle_down(band) > p_o, + available_bands, + ) + ) + )[-5:] + lower_bands = sorted( + list( + filter( + lambda band: self.market_amm.p_oracle_up(band) < p_o, + available_bands, + ) + ) + )[:5] available_bands = upper_bands + lower_bands if len(available_bands) > 0: - target_band = available_bands[int(target_band_pct * (len(available_bands) - 1))] + target_band = available_bands[ + int(target_band_pct * (len(available_bands) - 1)) + ] p_up = self.market_amm.p_oracle_up(target_band) p_down = self.market_amm.p_oracle_down(target_band) p_target = int(p_down + target_price_pct * (p_up - p_down)) self.price_oracle.set_price(p_target, sender=self.admin) amount, pump = self.market_amm.get_amount_for_price(p_target) - balance = self.stablecoin.balanceOf(self.chad) if pump else self.collateral_token.balanceOf(self.chad) + balance = ( + self.stablecoin.balanceOf(self.chad) + if pump + else self.collateral_token.balanceOf(self.chad) + ) amount = min(amount, balance) if amount > 0: if pump: @@ -180,7 +239,7 @@ def checkpoint(self, uid): r2 = self.integrals[user]["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 + assert r1 == pytest.approx(r2, rel=1e-13) or abs(r1 - r2) < 100 @rule(uid=user_id) def claim_crv(self, uid): @@ -204,11 +263,15 @@ def invariant_collateral(self): for account, integral in self.integrals.items(): y1 = self.lm_callback.user_collateral(account) y2 = integral["collateral"] - assert approx(y1, y2, 1e-14) or abs(y1 - y2) < 100000 # Seems ok for 18 decimals + assert ( + y1 == pytest.approx(y2, rel=1e-14) or abs(y1 - y2) < 100000 + ) # Seems ok for 18 decimals Y1 = self.lm_callback.total_collateral() Y2 = sum([i["collateral"] for i in self.integrals.values()]) - assert approx(Y1, Y2, 1e-13) or abs(Y1 - Y2) < 100000 # Seems ok for 18 decimals + assert ( + Y1 == pytest.approx(Y2, rel=1e-13) or abs(Y1 - Y2) < 100000 + ) # Seems ok for 18 decimals def teardown(self): """ @@ -219,17 +282,21 @@ def teardown(self): initial_collateral = self.collateral_token.balanceOf(account) collateral_in_amm = integral["collateral"] debt = self.market_controller.user_state(account)[2] - self.market_controller.repay(debt) + if debt > 0: + self.market_controller.repay(debt) self.update_integrals() assert not self.market_controller.loan_exists(account) - assert self.collateral_token.balanceOf(account) == initial_collateral + collateral_in_amm + assert ( + self.collateral_token.balanceOf(account) + == initial_collateral + collateral_in_amm + ) r1 = self.lm_callback.integrate_fraction(account) r2 = integral["integral"] assert (r1 > 0) == (r2 > 0) if r1 > 0: - assert approx(r1, r2, 1e-13) or abs(r1 - r2) < 100 + assert r1 == pytest.approx(r2, rel=1e-13) or abs(r1 - r2) < 100 crv_balance = self.crv.balanceOf(account) with boa.env.anchor(): @@ -239,25 +306,25 @@ def teardown(self): def test_state_machine( - accounts, - admin, - chad, - stablecoin, - collateral_token, - crv, - lm_callback, - market_controller, - market_amm, - price_oracle, - minter, + accounts, + admin, + chad, + stablecoin, + collateral_token, + crv, + lm_callback, + market_controller, + market_amm, + price_oracle, + minter, ): for acct in accounts[:5]: with boa.env.prank(admin): - collateral_token._mint_for_testing(acct, 1000 * 10**18) - crv.transfer(acct, 10 ** 20) + boa.deal(collateral_token, acct, 1000 * 10**18) + crv.transfer(acct, 10**20) with boa.env.prank(acct): - collateral_token.approve(market_controller, 2 ** 256 - 1) + collateral_token.approve(market_controller, 2**256 - 1) boa.env.time_travel(seconds=7 * 86400) diff --git a/tests_leverage/test_v1/__init__.py b/tests/price_oracles/__init__.py similarity index 100% rename from tests_leverage/test_v1/__init__.py rename to tests/price_oracles/__init__.py diff --git a/tests_leverage/test_v2/__init__.py b/tests/price_oracles/lp-oracles/__init__.py similarity index 100% rename from tests_leverage/test_v2/__init__.py rename to tests/price_oracles/lp-oracles/__init__.py diff --git a/tests/price_oracles/lp-oracles/conftest.py b/tests/price_oracles/lp-oracles/conftest.py new file mode 100644 index 00000000..f53aeb7f --- /dev/null +++ b/tests/price_oracles/lp-oracles/conftest.py @@ -0,0 +1,110 @@ +import boa +import pytest +import random +from tests.utils.deployers import ( + WETH_DEPLOYER, + DUMMY_PRICE_ORACLE_DEPLOYER, + MOCK_STABLE_SWAP_DEPLOYER, + MOCK_CRYPTO_SWAP_DEPLOYER, + MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER, + PROXY_ORACLE_DEPLOYER, + PROXY_ORACLE_FACTORY_DEPLOYER, + LP_ORACLE_STABLE_DEPLOYER, + LP_ORACLE_CRYPTO_DEPLOYER, + LP_ORACLE_FACTORY_DEPLOYER, +) + + +@pytest.fixture(scope="session") +def user(accounts): + return accounts[0] + + +@pytest.fixture(scope="module") +def broken_contract(admin): + with boa.env.prank(admin): + return WETH_DEPLOYER.deploy() + + +@pytest.fixture(scope="module") +def coin0_oracle(admin): + with boa.env.prank(admin): + return DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + admin, random.randint(99 * 10**17, 101 * 10**17) + ) + + +@pytest.fixture(scope="module") +def get_stable_swap(admin): + def f(N): + prices = [random.randint(10**16, 10**23) for i in range(N - 1)] + with boa.env.prank(admin): + return MOCK_STABLE_SWAP_DEPLOYER.deploy(admin, prices) + + return f + + +@pytest.fixture(scope="module") +def crypto_swap(admin): + with boa.env.prank(admin): + return MOCK_CRYPTO_SWAP_DEPLOYER.deploy(admin, random.randint(10**16, 10**23)) + + +@pytest.fixture(scope="module") +def stable_swap_no_argument(admin): + with boa.env.prank(admin): + return MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER.deploy( + admin, random.randint(10**16, 10**23) + ) + + +@pytest.fixture(scope="module") +def proxy_impl(admin): + with boa.env.prank(admin): + return PROXY_ORACLE_DEPLOYER.deploy() + + +@pytest.fixture(scope="module") +def proxy_factory(admin, proxy_impl): + with boa.env.prank(admin): + return PROXY_ORACLE_FACTORY_DEPLOYER.deploy(admin, proxy_impl) + + +@pytest.fixture(scope="module") +def lp_oracle_stable_impl(admin): + with boa.env.prank(admin): + return LP_ORACLE_STABLE_DEPLOYER.deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def lp_oracle_crypto_impl(admin): + with boa.env.prank(admin): + return LP_ORACLE_CRYPTO_DEPLOYER.deploy_as_blueprint() + + +@pytest.fixture(scope="module") +def lp_oracle_factory( + admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory +): + with boa.env.prank(admin): + return LP_ORACLE_FACTORY_DEPLOYER.deploy( + admin, lp_oracle_stable_impl, lp_oracle_crypto_impl, proxy_factory + ) + + +@pytest.fixture(scope="module") +def get_lp_oracle_stable(admin): + def f(pool, coin0_oracle): + with boa.env.prank(admin): + return LP_ORACLE_STABLE_DEPLOYER.deploy(pool, coin0_oracle) + + return f + + +@pytest.fixture(scope="module") +def get_lp_oracle_crypto(admin): + def f(pool, coin0_oracle): + with boa.env.prank(admin): + return LP_ORACLE_CRYPTO_DEPLOYER.deploy(pool, coin0_oracle) + + return f diff --git a/tests/price_oracles/lp-oracles/test_lp_oracle.py b/tests/price_oracles/lp-oracles/test_lp_oracle.py new file mode 100644 index 00000000..d7d09637 --- /dev/null +++ b/tests/price_oracles/lp-oracles/test_lp_oracle.py @@ -0,0 +1,83 @@ +import boa + + +def _get_lp_stable_price(stable_swap): + prices = [10**18] + for i in range(7): + try: + prices.append(stable_swap.price(i)) + except: + break + + return min(prices) + + +def test_lp_oracle_stable( + get_lp_oracle_stable, + get_stable_swap, + stable_swap_no_argument, + coin0_oracle, + broken_contract, + admin, +): + with boa.reverts(): + get_lp_oracle_stable(broken_contract, coin0_oracle) + with boa.reverts(): + get_lp_oracle_stable(stable_swap_no_argument, broken_contract) + with boa.reverts("Less than 2 coins"): + get_lp_oracle_stable(get_stable_swap(1), coin0_oracle) + + for N in range(1, 9): # 1 is for NO_ARGUMENT + stable_swap = get_stable_swap(N) if N > 1 else stable_swap_no_argument + + if N == 1: + with boa.env.anchor(): + stable_swap.set_price(0, sender=admin) + with boa.reverts("pool.price_oracle() returns 0"): + get_lp_oracle_stable(stable_swap, coin0_oracle) + else: + for i in range(N - 1): + with boa.env.anchor(): + stable_swap.set_price(i, 0, sender=admin) + with boa.reverts("pool.price_oracle(i) returns 0"): + get_lp_oracle_stable(stable_swap, coin0_oracle) + + with boa.env.anchor(): + coin0_oracle.set_price(0, sender=admin) + with boa.reverts("coin0_oracle.price() returns 0"): + get_lp_oracle_stable(stable_swap, coin0_oracle) + + oracle = get_lp_oracle_stable(stable_swap, coin0_oracle) + assert oracle.POOL() == stable_swap.address + assert ( + oracle.price() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price() // 10**18 + ) + assert ( + oracle.price_w() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price_w() // 10**18 + ) + + +def test_lp_oracle_crypto( + get_lp_oracle_crypto, crypto_swap, coin0_oracle, broken_contract, admin +): + with boa.reverts(): + get_lp_oracle_crypto(broken_contract, coin0_oracle) + with boa.reverts(): + get_lp_oracle_crypto(crypto_swap, broken_contract) + + with boa.env.anchor(): + crypto_swap.set_lp_price(0, sender=admin) + with boa.reverts("pool.lp_price() returns 0"): + get_lp_oracle_crypto(crypto_swap, coin0_oracle) + + with boa.env.anchor(): + coin0_oracle.set_price(0, sender=admin) + with boa.reverts("coin0_oracle.price() returns 0"): + get_lp_oracle_crypto(crypto_swap, coin0_oracle) + + oracle = get_lp_oracle_crypto(crypto_swap, coin0_oracle) + assert oracle.POOL() == crypto_swap.address + assert oracle.price() == crypto_swap.lp_price() * coin0_oracle.price() // 10**18 + assert oracle.price_w() == crypto_swap.lp_price() * coin0_oracle.price_w() // 10**18 diff --git a/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py new file mode 100644 index 00000000..e8fa2edb --- /dev/null +++ b/tests/price_oracles/lp-oracles/test_lp_oracle_factory.py @@ -0,0 +1,131 @@ +import boa +from tests.utils.deployers import LP_ORACLE_STABLE_DEPLOYER, LP_ORACLE_CRYPTO_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS + + +def test_lp_oracle_stable_factory( + lp_oracle_factory, + proxy_factory, + get_stable_swap, + stable_swap_no_argument, + coin0_oracle, + broken_contract, + admin, +): + with boa.reverts(): + lp_oracle_factory.deploy_oracle(broken_contract, coin0_oracle) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(stable_swap_no_argument, broken_contract) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(get_stable_swap(1), coin0_oracle) + + for N in range(1, 9): # 1 is for NO_ARGUMENT + stable_swap = get_stable_swap(N) if N > 1 else stable_swap_no_argument + + if N == 1: + with boa.env.anchor(): + stable_swap.set_price(0, sender=admin) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) + else: + for i in range(N - 1): + with boa.env.anchor(): + stable_swap.set_price(i, 0, sender=admin) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) + + with boa.env.anchor(): + coin0_oracle.set_price(0, sender=admin) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) + + with boa.env.anchor(): + oracle, proxy = lp_oracle_factory.deploy_oracle( + stable_swap, coin0_oracle, False + ) + oracle = LP_ORACLE_STABLE_DEPLOYER.at(oracle) + assert oracle.address == lp_oracle_factory.get_oracle( + stable_swap, coin0_oracle + ) + assert proxy == ZERO_ADDRESS + assert oracle.POOL() == stable_swap.address + assert ( + oracle.price() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price() // 10**18 + ) + assert ( + oracle.price_w() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price_w() // 10**18 + ) + + oracle, proxy = lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) + oracle = LP_ORACLE_STABLE_DEPLOYER.at(oracle) + assert oracle.address == lp_oracle_factory.get_oracle(stable_swap, coin0_oracle) + assert proxy == proxy_factory.get_proxy(oracle) + assert oracle.POOL() == stable_swap.address + assert ( + oracle.price() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price() // 10**18 + ) + assert ( + oracle.price_w() + == _get_lp_stable_price(stable_swap) * coin0_oracle.price_w() // 10**18 + ) + + with boa.reverts("Oracle already exists"): + lp_oracle_factory.deploy_oracle(stable_swap, coin0_oracle) + + +def test_lp_oracle_crypto_factory( + lp_oracle_factory, proxy_factory, crypto_swap, coin0_oracle, broken_contract, admin +): + with boa.reverts(): + lp_oracle_factory.deploy_oracle(broken_contract, coin0_oracle) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(crypto_swap, broken_contract) + + with boa.env.anchor(): + crypto_swap.set_lp_price(0, sender=admin) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + + with boa.env.anchor(): + coin0_oracle.set_price(0, sender=admin) + with boa.reverts(): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + + with boa.env.anchor(): + oracle, proxy = lp_oracle_factory.deploy_oracle( + crypto_swap, coin0_oracle, False + ) + oracle = LP_ORACLE_CRYPTO_DEPLOYER.at(oracle) + assert oracle.address == lp_oracle_factory.get_oracle(crypto_swap, coin0_oracle) + assert proxy == ZERO_ADDRESS + assert oracle.POOL() == crypto_swap.address + assert oracle.price() == crypto_swap.lp_price() * coin0_oracle.price() // 10**18 + assert ( + oracle.price_w() + == crypto_swap.lp_price() * coin0_oracle.price_w() // 10**18 + ) + + oracle, proxy = lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + oracle = LP_ORACLE_CRYPTO_DEPLOYER.at(oracle) + assert oracle.address == lp_oracle_factory.get_oracle(crypto_swap, coin0_oracle) + assert proxy == proxy_factory.get_proxy(oracle) + assert oracle.POOL() == crypto_swap.address + assert oracle.price() == crypto_swap.lp_price() * coin0_oracle.price() // 10**18 + assert oracle.price_w() == crypto_swap.lp_price() * coin0_oracle.price_w() // 10**18 + + with boa.reverts("Oracle already exists"): + lp_oracle_factory.deploy_oracle(crypto_swap, coin0_oracle) + + +def _get_lp_stable_price(stable_swap): + prices = [10**18] + for i in range(7): + try: + prices.append(stable_swap.price(i)) + except: + break + + return min(prices) diff --git a/tests_leverage/test_v2/tests/__init__.py b/tests/price_oracles/proxy/__init__.py similarity index 100% rename from tests_leverage/test_v2/tests/__init__.py rename to tests/price_oracles/proxy/__init__.py diff --git a/tests/price_oracles/proxy/conftest.py b/tests/price_oracles/proxy/conftest.py new file mode 100644 index 00000000..7948d95a --- /dev/null +++ b/tests/price_oracles/proxy/conftest.py @@ -0,0 +1,42 @@ +import boa +import pytest +from tests.utils.deployers import ( + DUMMY_PRICE_ORACLE_DEPLOYER, + WETH_DEPLOYER, + PROXY_ORACLE_DEPLOYER, + PROXY_ORACLE_FACTORY_DEPLOYER, +) + + +@pytest.fixture(scope="session") +def user(accounts): + return accounts[0] + + +@pytest.fixture(scope="module") +def get_price_oracle(admin): + def f(price): + with boa.env.prank(admin): + oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy(admin, price) + return oracle + + return f + + +@pytest.fixture(scope="module") +def broken_price_oracle(admin): + with boa.env.prank(admin): + oracle = WETH_DEPLOYER.deploy() + return oracle + + +@pytest.fixture(scope="module") +def proxy_impl(admin): + with boa.env.prank(admin): + return PROXY_ORACLE_DEPLOYER.deploy() + + +@pytest.fixture(scope="module") +def proxy_factory(admin, proxy_impl): + with boa.env.prank(admin): + return PROXY_ORACLE_FACTORY_DEPLOYER.deploy(admin, proxy_impl) diff --git a/tests/price_oracles/proxy/test_proxy.py b/tests/price_oracles/proxy/test_proxy.py new file mode 100644 index 00000000..e851ccdd --- /dev/null +++ b/tests/price_oracles/proxy/test_proxy.py @@ -0,0 +1,71 @@ +import boa +from tests.utils.deployers import PROXY_ORACLE_DEPLOYER +from tests.utils.constants import ZERO_ADDRESS + + +def test_proxy(proxy_factory, get_price_oracle, user, admin, broken_price_oracle): + zero_oracle = get_price_oracle(0) + oracle1 = get_price_oracle(100 * 10**18) + with boa.env.prank(user): + with boa.reverts(): + proxy_factory.deploy_proxy_oracle(broken_price_oracle) + with boa.reverts("price() call failed"): + proxy_factory.deploy_proxy_oracle(zero_oracle) + proxy_address = proxy_factory.deploy_proxy_oracle(oracle1) + proxy = PROXY_ORACLE_DEPLOYER.at(proxy_address) + + assert proxy.factory() == proxy_factory.address + assert proxy.oracle() == oracle1.address + assert proxy_factory.get_proxy(oracle1) == proxy.address + assert proxy.price() == oracle1.price() + assert proxy.price_w() == oracle1.price_w() + + # --- Change max deviation --- + + with boa.reverts("Not authorized"): + proxy.set_max_deviation(500, sender=user) + + with boa.env.prank(admin): + with boa.reverts("Invalid deviation"): + proxy.set_max_deviation(0) + with boa.reverts("Invalid deviation"): + proxy.set_max_deviation(5001) + + proxy.set_max_deviation(500) + + assert proxy.max_deviation() == 500 + + # --- Replace oracle --- + + oracle2 = get_price_oracle(105 * 10**18) + oracle_deviation_too_high = get_price_oracle(105 * 10**18 + 1) + with boa.reverts("Not authorized"): + proxy.set_price_oracle(oracle2, sender=admin) # Change only through the factory + with boa.reverts("ownable: caller is not the owner"): + proxy_factory.replace_oracle(proxy, oracle2, sender=user) + + with boa.env.prank(admin): + with boa.reverts(): + proxy_factory.replace_oracle(proxy, broken_price_oracle) + with boa.reverts("price() call failed"): + proxy_factory.replace_oracle(proxy, zero_oracle) + with boa.reverts("Price deviation too high"): + proxy_factory.replace_oracle(proxy, oracle_deviation_too_high) + with boa.env.anchor(): + proxy_factory.replace_oracle( + proxy, oracle_deviation_too_high, True + ) # skip deviation check + + assert proxy.oracle() == oracle_deviation_too_high.address + assert proxy_factory.get_proxy(oracle1) == ZERO_ADDRESS + assert proxy_factory.get_proxy(oracle_deviation_too_high) == proxy.address + assert proxy.price() == oracle_deviation_too_high.price() + assert proxy.price_w() == oracle_deviation_too_high.price_w() + + proxy_factory.replace_oracle(proxy, oracle2) + + assert proxy.oracle() == oracle2.address + assert proxy_factory.get_proxy(oracle1) == ZERO_ADDRESS + assert proxy_factory.get_proxy(oracle2) == proxy.address + assert proxy.price() == oracle2.price() + assert proxy.price_w() == oracle2.price_w() diff --git a/tests/stableborrow/conftest.py b/tests/stableborrow/conftest.py index 34f40ce2..5c7a7f89 100644 --- a/tests/stableborrow/conftest.py +++ b/tests/stableborrow/conftest.py @@ -1,97 +1,110 @@ import boa import pytest from vyper.utils import method_id +from tests.utils.deployers import ( + STABLECOIN_DEPLOYER, + WETH_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + MINT_CONTROLLER_DEPLOYER, + AMM_DEPLOYER, + CONSTANT_MONETARY_POLICY_DEPLOYER, + FAKE_LEVERAGE_DEPLOYER, +) def get_method_id(desc): - return method_id(desc).to_bytes(4, 'big') + b'\x00' * 28 + return method_id(desc).to_bytes(4, "big") + b"\x00" * 28 @pytest.fixture(scope="session") def stablecoin_pre(): - return boa.load_partial('contracts/Stablecoin.vy') + return STABLECOIN_DEPLOYER @pytest.fixture(scope="module") def stablecoin(stablecoin_pre, admin): with boa.env.prank(admin): - return stablecoin_pre.deploy('Curve USD', 'crvUSD') + return stablecoin_pre.deploy("Curve USD", "crvUSD") -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def weth(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/WETH.vy') + return WETH_DEPLOYER.deploy() @pytest.fixture(scope="session") def controller_factory_impl(): - return boa.load_partial('contracts/ControllerFactory.vy') + return CONTROLLER_FACTORY_DEPLOYER @pytest.fixture(scope="module") def controller_prefactory(controller_factory_impl, stablecoin, weth, admin, accounts): with boa.env.prank(admin): - return controller_factory_impl.deploy(stablecoin.address, admin, accounts[0], weth.address) - - -@pytest.fixture(scope="session") -def controller_interface(): - return boa.load_partial('contracts/Controller.vy') - - -@pytest.fixture(scope="session") -def controller_impl(controller_interface, admin): - with boa.env.prank(admin): - return controller_interface.deploy_as_blueprint() + return controller_factory_impl.deploy( + stablecoin.address, admin, accounts[0], weth.address + ) -@pytest.fixture(scope="session") -def amm_interface(): - return boa.load_partial('contracts/AMM.vy') +@pytest.fixture(scope="module") +def controller_impl(proto): + return proto.blueprints.mint_controller -@pytest.fixture(scope="session") -def amm_impl(amm_interface, admin): +@pytest.fixture(scope="module") +def amm_impl(admin): with boa.env.prank(admin): - return amm_interface.deploy_as_blueprint() + return AMM_DEPLOYER.deploy_as_blueprint() @pytest.fixture(scope="module") -def controller_factory(controller_prefactory, amm_impl, controller_impl, stablecoin, admin): +def controller_factory( + controller_prefactory, amm_impl, controller_impl, stablecoin, admin +): with boa.env.prank(admin): - controller_prefactory.set_implementations(controller_impl.address, amm_impl.address) + controller_prefactory.set_implementations( + controller_impl.address, amm_impl.address + ) stablecoin.set_minter(controller_prefactory.address) return controller_prefactory -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def monetary_policy(admin): with boa.env.prank(admin): - policy = boa.load('contracts/testing/ConstantMonetaryPolicy.vy', admin) + policy = CONSTANT_MONETARY_POLICY_DEPLOYER.deploy(admin) policy.set_rate(0) return policy @pytest.fixture(scope="module") -def get_market(controller_factory, monetary_policy, price_oracle, stablecoin, accounts, admin): +def get_market( + controller_factory, monetary_policy, price_oracle, stablecoin, accounts, admin +): def f(collateral_token): with boa.env.prank(admin): if controller_factory.n_collaterals() == 0: controller_factory.add_market( - collateral_token.address, 100, 10**16, 0, + collateral_token.address, + 100, + 10**16, + 0, price_oracle.address, - monetary_policy.address, 5 * 10**16, 2 * 10**16, - 10**6 * 10**18) + monetary_policy.address, + 5 * 10**16, + 2 * 10**16, + 10**6 * 10**18, + ) amm = controller_factory.get_amm(collateral_token.address) controller = controller_factory.get_controller(collateral_token.address) for acc in accounts: with boa.env.prank(acc): - collateral_token.approve(amm, 2**256-1) - stablecoin.approve(amm, 2**256-1) - collateral_token.approve(controller, 2**256-1) - stablecoin.approve(controller, 2**256-1) + collateral_token.approve(amm, 2**256 - 1) + stablecoin.approve(amm, 2**256 - 1) + collateral_token.approve(controller, 2**256 - 1) + stablecoin.approve(controller, 2**256 - 1) return controller_factory + return f @@ -101,13 +114,13 @@ def market(get_market, collateral_token): @pytest.fixture(scope="module") -def market_amm(market, collateral_token, stablecoin, amm_interface, accounts): - return amm_interface.at(market.get_amm(collateral_token.address)) +def market_amm(market, collateral_token, stablecoin, accounts): + return AMM_DEPLOYER.at(market.get_amm(collateral_token.address)) @pytest.fixture(scope="module") -def market_controller(market, stablecoin, collateral_token, controller_interface, accounts): - return controller_interface.at(market.get_controller(collateral_token.address)) +def market_controller(market, stablecoin, collateral_token, accounts): + return MINT_CONTROLLER_DEPLOYER.at(market.get_controller(collateral_token.address)) @pytest.fixture(scope="module") @@ -115,10 +128,19 @@ def get_fake_leverage(stablecoin, admin): def f(collateral_token, market_controller): # Fake leverage testing contract can also be used to liquidate via the callback with boa.env.prank(admin): - leverage = boa.load('contracts/testing/FakeLeverage.vy', stablecoin.address, collateral_token.address, - market_controller.address, 3000 * 10**18) - collateral_token._mint_for_testing(leverage.address, 1000 * 10**collateral_token.decimals()) + leverage = FAKE_LEVERAGE_DEPLOYER.deploy( + stablecoin.address, + collateral_token.address, + market_controller.address, + 3000 * 10**18, + ) + boa.deal( + collateral_token, + leverage.address, + 1000 * 10 ** collateral_token.decimals(), + ) return leverage + return f diff --git a/tests/stableborrow/stabilize/conftest.py b/tests/stableborrow/stabilize/conftest.py index e654eec6..fbe84299 100644 --- a/tests/stableborrow/stabilize/conftest.py +++ b/tests/stableborrow/stabilize/conftest.py @@ -1,8 +1,28 @@ import boa import pytest -from ...conftest import approx +from tests.utils.deployers import ( + ERC20_MOCK_DEPLOYER, + STABLESWAP_DEPLOYER, + SWAP_FACTORY_DEPLOYER, + MOCK_RATE_ORACLE_DEPLOYER, + CURVE_STABLESWAP_FACTORY_NG_DEPLOYER, + CURVE_STABLESWAP_NG_DEPLOYER, + CURVE_STABLESWAP_NG_MATH_DEPLOYER, + CURVE_STABLESWAP_NG_VIEWS_DEPLOYER, + AGGREGATE_STABLE_PRICE3_DEPLOYER, + TRICRYPTO_MOCK_DEPLOYER, + CRYPTO_WITH_STABLE_PRICE_DEPLOYER, + CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER, + MOCK_PEG_KEEPER_DEPLOYER, + PEG_KEEPER_REGULATOR_DEPLOYER, + PEG_KEEPER_V2_DEPLOYER, + AGG_MONETARY_POLICY2_DEPLOYER, + CHAINLINK_AGGREGATOR_MOCK_DEPLOYER, + AMM_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, +) +from tests.utils.constants import ZERO_ADDRESS -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" BASE_AMOUNT = 10**6 @@ -28,56 +48,55 @@ def bob(accounts): @pytest.fixture(scope="module") -def collateral_token(get_collateral_token): - return get_collateral_token(18) +def collateral_token(): + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") def stablecoin_a(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20Mock.vy', "USDa", "USDa", 6) + return ERC20_MOCK_DEPLOYER.deploy(6) @pytest.fixture(scope="module") def stablecoin_b(admin): with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20Mock.vy', "USDb", "USDb", 18) + return ERC20_MOCK_DEPLOYER.deploy(18) @pytest.fixture(scope="module") def swap_impl(admin): with boa.env.prank(admin): - return boa.load('contracts/Stableswap.vy') + return STABLESWAP_DEPLOYER.deploy() @pytest.fixture(scope="module") def swap_deployer(swap_impl, admin): with boa.env.prank(admin): - deployer = boa.load('contracts/testing/SwapFactory.vy', swap_impl.address) + deployer = SWAP_FACTORY_DEPLOYER.deploy(swap_impl.address) return deployer @pytest.fixture(scope="module") def rate_oracle(swap_impl, admin): - return boa.load('contracts/testing/MockRateOracle.vy') + return MOCK_RATE_ORACLE_DEPLOYER.deploy() @pytest.fixture(scope="module") def swap_impl_ng(admin, swap_deployer, rate_oracle): with boa.env.prank(admin): # Do not forget `git submodule init` and `git submodule update` - prefix = "contracts/testing/stableswap-ng/contracts/main" - factory = boa.load(f"{prefix}/CurveStableSwapFactoryNG.vy", admin, admin) - swap_deployer.eval(f'self.factory_ng = FactoryNG({factory.address})') - swap_deployer.eval(f'self.rate_oracle = {rate_oracle.address}') + factory = CURVE_STABLESWAP_FACTORY_NG_DEPLOYER.deploy(admin, admin) + swap_deployer.eval(f"self.factory_ng = FactoryNG({factory.address})") + swap_deployer.eval(f"self.rate_oracle = {rate_oracle.address}") - impl = boa.load_partial(f"{prefix}/CurveStableSwapNG.vy").deploy_as_blueprint() + impl = CURVE_STABLESWAP_NG_DEPLOYER.deploy_as_blueprint() factory.set_pool_implementations(0, impl) - math = boa.load(f"{prefix}/CurveStableSwapNGMath.vy") + math = CURVE_STABLESWAP_NG_MATH_DEPLOYER.deploy() factory.set_math_implementation(math) - views = boa.load(f"{prefix}/CurveStableSwapNGViews.vy") + views = CURVE_STABLESWAP_NG_VIEWS_DEPLOYER.deploy() factory.set_views_implementation(views) return impl @@ -92,7 +111,9 @@ def unsafe_factory(controller_factory, stablecoin, admin, accounts): @pytest.fixture(scope="module") -def stableswap_a(unsafe_factory, swap_deployer, swap_impl, stablecoin, stablecoin_a, admin): +def stableswap_a( + unsafe_factory, swap_deployer, swap_impl, stablecoin, stablecoin_a, admin +): with boa.env.prank(admin): addr = swap_deployer.deploy(stablecoin_a, stablecoin) swap = swap_impl.deployer.at(addr) @@ -100,7 +121,9 @@ def stableswap_a(unsafe_factory, swap_deployer, swap_impl, stablecoin, stablecoi @pytest.fixture(scope="module") -def stableswap_b(unsafe_factory, swap_deployer, swap_impl_ng, stablecoin, stablecoin_b, admin): +def stableswap_b( + unsafe_factory, swap_deployer, swap_impl_ng, stablecoin, stablecoin_b, admin +): with boa.env.prank(admin): addr = swap_deployer.deploy_ng(stablecoin_b, stablecoin) swap = swap_impl_ng.deployer.at(addr) @@ -118,6 +141,7 @@ def inner(swap, fee, offpeg_fee_multiplier=None): swap.eval(f"self.fee = {fee}") if offpeg_fee_multiplier: swap.eval(f"self.offpeg_fee_multiplier = {offpeg_fee_multiplier}") + return inner @@ -129,7 +153,7 @@ def redeemable_tokens(stablecoin_a, stablecoin_b): @pytest.fixture(scope="module") def price_aggregator(stablecoin, stableswap_a, stableswap_b, admin): with boa.env.prank(admin): - agg = boa.load('contracts/price_oracles/AggregateStablePrice3.vy', stablecoin.address, 10**15, admin) + agg = AGGREGATE_STABLE_PRICE3_DEPLOYER.deploy(stablecoin.address, 10**15, admin) agg.add_price_pair(stableswap_a.address) agg.add_price_pair(stableswap_b.address) return agg @@ -138,26 +162,37 @@ def price_aggregator(stablecoin, stableswap_a, stableswap_b, admin): @pytest.fixture(scope="module") def dummy_tricrypto(stablecoin_a, admin): with boa.env.prank(admin): - pool = boa.load('contracts/testing/TricryptoMock.vy', - [stablecoin_a.address, - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"]) + pool = TRICRYPTO_MOCK_DEPLOYER.deploy( + [ + stablecoin_a.address, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + ] + ) pool.set_price(0, 3000 * 10**18) pool.set_price(1, 20000 * 10**18) return pool @pytest.fixture(scope="module") -def agg(stablecoin, stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, price_aggregator, admin): +def agg( + stablecoin, + stablecoin_a, + stablecoin_b, + stableswap_a, + stableswap_b, + price_aggregator, + admin, +): with boa.env.anchor(): with boa.env.prank(admin): - stablecoin_a._mint_for_testing(admin, 500000 * 10**6) - stablecoin_b._mint_for_testing(admin, 500000 * 10**18) + boa.deal(stablecoin_a, admin, 500000 * 10**6) + boa.deal(stablecoin_b, admin, 500000 * 10**18) - stablecoin_a.approve(stableswap_a.address, 2**256-1) - stablecoin.approve(stableswap_a.address, 2**256-1) - stablecoin_b.approve(stableswap_b.address, 2**256-1) - stablecoin.approve(stableswap_b.address, 2**256-1) + stablecoin_a.approve(stableswap_a.address, 2**256 - 1) + stablecoin.approve(stableswap_a.address, 2**256 - 1) + stablecoin_b.approve(stableswap_b.address, 2**256 - 1) + stablecoin.approve(stableswap_b.address, 2**256 - 1) stableswap_a.add_liquidity([500000 * 10**6, 500000 * 10**18], 0) stableswap_b.add_liquidity([500000 * 10**18, 500000 * 10**18], 0) @@ -167,30 +202,26 @@ def agg(stablecoin, stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, pric @pytest.fixture(scope="module") def crypto_agg(dummy_tricrypto, agg, stableswap_a, admin): with boa.env.prank(admin): - crypto_agg = boa.load( - 'contracts/price_oracles/CryptoWithStablePrice.vy', - dummy_tricrypto.address, - 0, - stableswap_a, - agg, - 5000 + crypto_agg = CRYPTO_WITH_STABLE_PRICE_DEPLOYER.deploy( + dummy_tricrypto.address, 0, stableswap_a, agg, 5000 ) crypto_agg.price_w() return crypto_agg @pytest.fixture(scope="module") -def crypto_agg_with_external_oracle(dummy_tricrypto, agg, stableswap_a, chainlink_price_oracle, admin): +def crypto_agg_with_external_oracle( + dummy_tricrypto, agg, stableswap_a, chainlink_price_oracle, admin +): with boa.env.prank(admin): - crypto_agg = boa.load( - 'contracts/price_oracles/CryptoWithStablePriceAndChainlink.vy', + crypto_agg = CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER.deploy( dummy_tricrypto.address, 0, stableswap_a, agg, chainlink_price_oracle.address, 5000, - 1 + 1, ) crypto_agg.price_w() return crypto_agg @@ -198,35 +229,46 @@ def crypto_agg_with_external_oracle(dummy_tricrypto, agg, stableswap_a, chainlin @pytest.fixture(scope="module") def mock_peg_keepers(stablecoin): - """ Make Regulator always pass order of prices check """ + """Make Regulator always pass order of prices check""" return [ - boa.load('contracts/testing/MockPegKeeper.vy', price, stablecoin) for price in [0, 2 ** 256 - 1] + MOCK_PEG_KEEPER_DEPLOYER.deploy(price, stablecoin) for price in [0, 2**256 - 1] ] @pytest.fixture(scope="module") def reg(agg, stablecoin, mock_peg_keepers, receiver, admin): - regulator = boa.load( - 'contracts/stabilizer/PegKeeperRegulator.vy', + regulator = PEG_KEEPER_REGULATOR_DEPLOYER.deploy( stablecoin, agg, receiver, admin, admin ) with boa.env.prank(admin): - regulator.set_price_deviation(10 ** 20) - regulator.set_debt_parameters(10 ** 18, 10 ** 18) + regulator.set_price_deviation(10**20) + regulator.set_debt_parameters(10**18, 10**18) regulator.add_peg_keepers([mock.address for mock in mock_peg_keepers]) return regulator @pytest.fixture(scope="module") -def peg_keepers(stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, controller_factory, reg, admin, receiver): +def peg_keepers( + stablecoin_a, + stablecoin_b, + stableswap_a, + stableswap_b, + controller_factory, + reg, + admin, + receiver, +): pks = [] with boa.env.prank(admin): - for (coin, pool) in [(stablecoin_a, stableswap_a), (stablecoin_b, stableswap_b)]: + for coin, pool in [(stablecoin_a, stableswap_a), (stablecoin_b, stableswap_b)]: pks.append( - boa.load( - 'contracts/stabilizer/PegKeeperV2.vy', - pool.address, 2 * 10**4, - controller_factory.address, reg.address, admin) + PEG_KEEPER_V2_DEPLOYER.deploy( + pool.address, + 2 * 10**4, + controller_factory.address, + reg.address, + admin, + ) ) reg.add_peg_keepers([pk.address for pk in pks]) return pks @@ -235,56 +277,81 @@ def peg_keepers(stablecoin_a, stablecoin_b, stableswap_a, stableswap_b, controll @pytest.fixture(scope="module") def agg_monetary_policy(peg_keepers, agg, controller_factory, admin): with boa.env.prank(admin): - mp = boa.load( - 'contracts/mpolicies/AggMonetaryPolicy2.vy', - admin, - agg.address, - controller_factory.address, - [p.address for p in peg_keepers] + [ZERO_ADDRESS] * 3, - 0, # Rate - 2 * 10**16, # Sigma 2% - 5 * 10**16) # Target debt fraction 5% + mp = AGG_MONETARY_POLICY2_DEPLOYER.deploy( + admin, + agg.address, + controller_factory.address, + [p.address for p in peg_keepers] + [ZERO_ADDRESS] * 3, + 0, # Rate + 2 * 10**16, # Sigma 2% + 5 * 10**16, + ) # Target debt fraction 5% mp.rate_write() return mp @pytest.fixture(scope="module") -def market_agg(controller_factory, collateral_token, agg_monetary_policy, crypto_agg, peg_keepers, admin): +def market_agg( + controller_factory, + collateral_token, + agg_monetary_policy, + crypto_agg, + peg_keepers, + admin, +): with boa.env.prank(admin): controller_factory.add_market( - collateral_token.address, 100, 10**16, 0, + collateral_token.address, + 100, + 10**16, + 0, crypto_agg.address, - agg_monetary_policy.address, 5 * 10**16, 2 * 10**16, - 10**8 * 10**18) + agg_monetary_policy.address, + 5 * 10**16, + 2 * 10**16, + 10**8 * 10**18, + ) for pk in peg_keepers: controller_factory.set_debt_ceiling(pk.address, 10**8 * 10**18) return controller_factory @pytest.fixture(scope="module") -def market_amm_agg(market, collateral_token, stablecoin, amm_impl, amm_interface, accounts): - amm = amm_interface.at(market.get_amm(collateral_token.address)) +def market_amm_agg(market, collateral_token, stablecoin, amm_impl, accounts): + amm = AMM_DEPLOYER.at(market.get_amm(collateral_token.address)) for acc in accounts: with boa.env.prank(acc): - collateral_token.approve(amm.address, 2**256-1) - stablecoin.approve(amm.address, 2**256-1) + collateral_token.approve(amm.address, 2**256 - 1) + stablecoin.approve(amm.address, 2**256 - 1) return amm @pytest.fixture(scope="module") -def market_controller_agg(market_agg, stablecoin, collateral_token, controller_impl, controller_interface, controller_factory, accounts): - controller = controller_interface.at(market_agg.get_controller(collateral_token.address)) +def market_controller_agg( + market_agg, + stablecoin, + collateral_token, + controller_impl, + controller_factory, + accounts, +): + controller = LEND_CONTROLLER_DEPLOYER.at( + market_agg.get_controller(collateral_token.address) + ) for acc in accounts: with boa.env.prank(acc): - collateral_token.approve(controller.address, 2**256-1) - stablecoin.approve(controller.address, 2**256-1) + collateral_token.approve(controller.address, 2**256 - 1) + stablecoin.approve(controller.address, 2**256 - 1) return controller @pytest.fixture(scope="module") def initial_amounts(redeemable_tokens, stablecoin): - stablecoin_amount = BASE_AMOUNT * 10**stablecoin.decimals() - return [(BASE_AMOUNT * 10**redeemable.decimals(), stablecoin_amount) for redeemable in redeemable_tokens] + stablecoin_amount = BASE_AMOUNT * 10 ** stablecoin.decimals() + return [ + (BASE_AMOUNT * 10 ** redeemable.decimals(), stablecoin_amount) + for redeemable in redeemable_tokens + ] @pytest.fixture(scope="module") @@ -295,22 +362,37 @@ def f(acct, coins, amounts): if amount > 0: if coin == stablecoin: collateral_amount = amount * 50 // 3000 - collateral_token._mint_for_testing(acct, collateral_amount) + boa.deal(collateral_token, acct, collateral_amount) if market_controller_agg.debt(acct) == 0: - collateral_token.approve(market_controller_agg.address, 2**256 - 1) - market_controller_agg.create_loan(collateral_amount, amount, 5) + collateral_token.approve( + market_controller_agg.address, 2**256 - 1 + ) + market_controller_agg.create_loan( + collateral_amount, amount, 5 + ) else: market_controller_agg.borrow_more(collateral_amount, amount) else: - coin._mint_for_testing(acct, amount) + boa.deal(coin, acct, amount) + return f @pytest.fixture(scope="module") def add_initial_liquidity( - initial_amounts, stablecoin, redeemable_tokens, swaps, collateral_token, market_controller_agg, alice, _mint): + initial_amounts, + stablecoin, + redeemable_tokens, + swaps, + collateral_token, + market_controller_agg, + alice, + _mint, +): with boa.env.prank(alice): - for (amount_r, amount_s), redeemable, pool in zip(initial_amounts, redeemable_tokens, swaps): + for (amount_r, amount_s), redeemable, pool in zip( + initial_amounts, redeemable_tokens, swaps + ): _mint(alice, [redeemable, stablecoin], [amount_r, amount_s]) stablecoin.approve(pool.address, 2**256 - 1) redeemable.approve(pool.address, 2**256 - 1) @@ -318,13 +400,17 @@ def add_initial_liquidity( @pytest.fixture(scope="module") -def provide_token_to_peg_keepers_no_sleep(initial_amounts, swaps, peg_keepers, redeemable_tokens, alice, peg_keeper_updater): - for (amount_r, amount_s), swap, pk, rtoken in zip(initial_amounts, swaps, peg_keepers, redeemable_tokens): +def provide_token_to_peg_keepers_no_sleep( + initial_amounts, swaps, peg_keepers, redeemable_tokens, alice, peg_keeper_updater +): + for (amount_r, amount_s), swap, pk, rtoken in zip( + initial_amounts, swaps, peg_keepers, redeemable_tokens + ): with boa.env.prank(alice): # Mint necessary amount of redeemable token rtoken.approve(pk.address, 2**256 - 1) amount = amount_r * 5 - rtoken._mint_for_testing(alice, amount) + boa.deal(rtoken, alice, amount) # Add redeemable token's liquidity to the stableswap pool swap.add_liquidity([amount, 0], 0) @@ -334,9 +420,13 @@ def provide_token_to_peg_keepers_no_sleep(initial_amounts, swaps, peg_keepers, r with boa.env.prank(alice): rtoken_mul = 10 ** (18 - rtoken.decimals()) - remove_amount = (swap.balances(0) * rtoken_mul - swap.balances(1)) // rtoken_mul + remove_amount = ( + swap.balances(0) * rtoken_mul - swap.balances(1) + ) // rtoken_mul swap.remove_liquidity_imbalance([remove_amount, 0], 2**256 - 1) - assert approx(swap.balances(0), swap.balances(1) // rtoken_mul, 1e-6) + assert swap.balances(0) == pytest.approx( + swap.balances(1) // rtoken_mul, rel=1e-6 + ) @pytest.fixture(scope="module") @@ -346,14 +436,28 @@ def provide_token_to_peg_keepers(provide_token_to_peg_keepers_no_sleep): @pytest.fixture(scope="module") def imbalance_pool( - initial_amounts, redeemable_tokens, stablecoin, collateral_token, market_controller_agg, alice, _mint): + initial_amounts, + redeemable_tokens, + stablecoin, + collateral_token, + market_controller_agg, + alice, + _mint, +): def _inner(swap, i, amount=None, add_diff=False): with boa.env.prank(alice): - rtoken, initial = [(r, i) for r, i in zip(redeemable_tokens, initial_amounts) if r.address == swap.coins(0)][0] + rtoken, initial = [ + (r, i) + for r, i in zip(redeemable_tokens, initial_amounts) + if r.address == swap.coins(0) + ][0] token_mul = [10 ** (18 - rtoken.decimals()), 1] amounts = [0, 0] if add_diff: - amount += (swap.balances(1 - i) * token_mul[1 - i] - swap.balances(i) * token_mul[i]) // token_mul[i] + amount += ( + swap.balances(1 - i) * token_mul[1 - i] + - swap.balances(i) * token_mul[i] + ) // token_mul[i] amounts[i] = amount or initial[i] // 3 _mint(alice, [rtoken, stablecoin], amounts) swap.add_liquidity(amounts, 0) @@ -363,14 +467,25 @@ def _inner(swap, i, amount=None, add_diff=False): @pytest.fixture(scope="module") def imbalance_pools( - swaps, initial_amounts, redeemable_tokens, stablecoin, collateral_token, market_controller_agg, alice, _mint): + swaps, + initial_amounts, + redeemable_tokens, + stablecoin, + collateral_token, + market_controller_agg, + alice, + _mint, +): def _inner(i, amount=None, add_diff=False): with boa.env.prank(alice): for initial, swap, rtoken in zip(initial_amounts, swaps, redeemable_tokens): token_mul = [10 ** (18 - rtoken.decimals()), 1] amounts = [0, 0] if add_diff: - amount += (swap.balances(1 - i) * token_mul[1 - i] - swap.balances(i) * token_mul[i]) // token_mul[i] + amount += ( + swap.balances(1 - i) * token_mul[1 - i] + - swap.balances(i) * token_mul[i] + ) // token_mul[i] amounts[i] = amount or initial[i] // 3 _mint(alice, [rtoken, stablecoin], amounts) swap.add_liquidity(amounts, 0) @@ -398,4 +513,4 @@ def mint_alice(alice, stablecoin, redeemable_tokens, swaps, initial_amounts, _mi @pytest.fixture(scope="module") def chainlink_price_oracle(admin): - return boa.load('contracts/testing/ChainlinkAggregatorMock.vy', 8, admin, 1000) + return CHAINLINK_AGGREGATOR_MOCK_DEPLOYER.deploy(8, admin, 1000) diff --git a/tests/stableborrow/stabilize/stateful/base.py b/tests/stableborrow/stabilize/stateful/base.py index 892a4887..3df7fabd 100644 --- a/tests/stableborrow/stabilize/stateful/base.py +++ b/tests/stableborrow/stabilize/stateful/base.py @@ -18,14 +18,17 @@ def __init__(self): super().__init__() self.profit = [pk.calc_profit() for pk in self.peg_keepers] stablecoin_decimals = self.stablecoin.decimals() - self.dmul = [[10 ** r.decimals(), 10 ** stablecoin_decimals] for r in self.redeemable_tokens] + self.dmul = [ + [10 ** r.decimals(), 10**stablecoin_decimals] + for r in self.redeemable_tokens + ] def _disable_fees(self): self.fees = [] with boa.env.prank(self.admin): for swap in self.swaps: self.fees.append(swap.fee()) - swap.eval(f"self.fee = 0") + swap.eval("self.fee = 0") def _enable_fees(self): with boa.env.prank(self.admin): @@ -92,8 +95,7 @@ def remove_imbalance(self, amount_0, amount_1, pool_idx): if token_amount > self.swaps[pool_idx].balanceOf(self.alice): return with boa.env.prank(self.alice): - self.swaps[pool_idx].remove_liquidity_imbalance( - amounts, 2**256 - 1) + self.swaps[pool_idx].remove_liquidity_imbalance(amounts, 2**256 - 1) @rule(pct=st_pct, pool_idx=st_pool) def remove(self, pct, pool_idx): diff --git a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py index c371847f..98f220c4 100644 --- a/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py +++ b/tests/stableborrow/stabilize/stateful/test_agg_monetary_policy.py @@ -1,26 +1,40 @@ from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, initialize, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + initialize, + rule, + invariant, +) from boa.interpret import VyperContract import boa +from tests.utils.deployers import ( + AGG_MONETARY_POLICY2_DEPLOYER, + ERC20_MOCK_DEPLOYER, + PEG_KEEPER_V2_DEPLOYER, + AGGREGATE_STABLE_PRICE3_DEPLOYER, + PEG_KEEPER_REGULATOR_DEPLOYER, +) +from tests.utils.constants import ZERO_ADDRESS - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" RATE0 = 634195839 # 2% class AggMonetaryPolicyCreation(RuleBasedStateMachine): digits = st.integers(min_value=6, max_value=18) - many_digits = st.lists(st.integers(min_value=6, max_value=18), min_size=1, max_size=5) + many_digits = st.lists( + st.integers(min_value=6, max_value=18), min_size=1, max_size=5 + ) deposit_amount = st.floats(min_value=1, max_value=1e9) deposit_split = st.floats(min_value=0.5, max_value=1.5) pool_number = st.integers(min_value=0, max_value=10000) rate = st.integers(min_value=0, max_value=43959106799) sigma = st.integers(min_value=10**14, max_value=10**18) target_debt_fraction = st.integers(min_value=1, max_value=10**18) - MPOLICY = boa.load_partial('contracts/mpolicies/AggMonetaryPolicy2.vy') - ERC20 = boa.load_partial('contracts/testing/ERC20Mock.vy') - PK = boa.load_partial('contracts/stabilizer/PegKeeperV2.vy') + MPOLICY = AGG_MONETARY_POLICY2_DEPLOYER + ERC20 = ERC20_MOCK_DEPLOYER + PK = PEG_KEEPER_V2_DEPLOYER def __init__(self): super().__init__() @@ -32,8 +46,12 @@ def __init__(self): self.one_usd = [] self.swaps = [] self.peg_keepers = [] - self.agg = boa.load('contracts/price_oracles/AggregateStablePrice3.vy', self.stablecoin.address, 10**15, self.admin) - self.reg = boa.load('contracts/stabilizer/PegKeeperRegulator.vy', self.stablecoin.address, self.agg, self.admin, self.admin, self.admin) + self.agg = AGGREGATE_STABLE_PRICE3_DEPLOYER.deploy( + self.stablecoin.address, 10**15, self.admin + ) + self.reg = PEG_KEEPER_REGULATOR_DEPLOYER.deploy( + self.stablecoin.address, self.agg, self.admin, self.admin, self.admin + ) @initialize(digits=many_digits) def initializer(self, digits): @@ -41,6 +59,16 @@ def initializer(self, digits): self.add_stablecoin(d) with boa.env.prank(self.admin): self.agg.add_price_pair(self.swaps[-1].address) + # Seed minimal liquidity so pools are never empty when EMA TVL updates + # This prevents aggregator from having get_virtual_price() return 0 + fed = self.stablecoins[-1] + swap = self.swaps[-1] + # Deposit ~1 unit of each side + amt_fed = self.one_usd[-1] # 1 unit in fedUSD base units + amt_crvusd = 10**18 # 1 crvUSD + boa.deal(fed, self.admin, amt_fed) + boa.deal(self.stablecoin, self.admin, amt_crvusd) + swap.add_liquidity([amt_fed, amt_crvusd], 0) self.mp = self.MPOLICY.deploy( self.admin, self.agg.address, @@ -48,26 +76,28 @@ def initializer(self, digits): [p.address for p in self.peg_keepers] + [ZERO_ADDRESS] * (5 - len(digits)), RATE0, 2 * 10**16, # Sigma 2% - 5 * 10**16) # Target debt fraction 5% + 5 * 10**16, + ) # Target debt fraction 5% def add_stablecoin(self, digits): with boa.env.prank(self.admin): # Deploy a stablecoin - fedUSD = self.ERC20.deploy("USD%s" % digits, "USD%s" % digits, digits) + fedUSD = self.ERC20.deploy(digits) # Deploy a swap n = self.swap_deployer.n() self.swap_deployer.deploy(fedUSD, self.stablecoin) addr = self.swap_deployer.pools(n) - swap = VyperContract( - self.swap_impl.compiler_data, - override_address=addr - ) + swap = VyperContract(self.swap_impl.compiler_data, override_address=addr) fedUSD.approve(swap.address, 2**256 - 1) self.stablecoin.approve(swap.address, 2**256 - 1) # Deploy a peg keeper pk = self.PK.deploy( - swap.address, 5 * 10**4, - self.controller_factory.address, self.reg.address, self.admin) + swap.address, + 5 * 10**4, + self.controller_factory.address, + self.reg.address, + self.admin, + ) self.stablecoins.append(fedUSD) self.swaps.append(swap) self.peg_keepers.append(pk) @@ -112,7 +142,8 @@ def deposit(self, amount, split, _n): n = _n % len(self.swaps) x = [int(amount * self.one_usd[n]), int(split * amount * 1e18)] with boa.env.prank(self.admin): - self.stablecoins[n]._mint_for_testing(self.admin, 2 * x[0]) + boa.deal(self.stablecoins[n], self.admin, 2 * x[0]) + boa.deal(self.stablecoin, self.admin, 2 * x[1]) self.swaps[n].add_liquidity(x, 0) # Add twice to record the price for MA self.swaps[n].add_liquidity(x, 0) @@ -127,11 +158,15 @@ def agg_price_readable(self): def rate_readable(self): rate = self.mp.rate() rate0 = self.mp.rate0() - assert abs((rate - rate0) / (rate0 + RATE0 // 1000)) < 1e6 # Can be huge at small sigma! + assert ( + abs((rate - rate0) / (rate0 + RATE0 // 1000)) < 1e6 + ) # Can be huge at small sigma! def test_agg_mp(unsafe_factory, swap_deployer, swap_impl, stablecoin, admin): - AggMonetaryPolicyCreation.TestCase.settings = settings(max_examples=30, stateful_step_count=20) + AggMonetaryPolicyCreation.TestCase.settings = settings( + max_examples=30, stateful_step_count=20 + ) for k, v in locals().items(): setattr(AggMonetaryPolicyCreation, k, v) run_state_machine_as_test(AggMonetaryPolicyCreation) diff --git a/tests/stableborrow/stabilize/stateful/test_diff.py b/tests/stableborrow/stabilize/stateful/test_diff.py index 55458350..b44cbdd7 100644 --- a/tests/stableborrow/stabilize/stateful/test_diff.py +++ b/tests/stableborrow/stabilize/stateful/test_diff.py @@ -7,10 +7,7 @@ from . import base -pytestmark = pytest.mark.usefixtures( - "add_initial_liquidity", - "mint_alice" -) +pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_alice") class StateMachine(base.StateMachine): @@ -24,7 +21,9 @@ def invariant_check_diff(self): """ Verify that Peg Keeper decreased diff of balances by 1/5. """ - for idx, (peg_keeper, swap, dmul) in enumerate(zip(self.peg_keepers, self.swaps, self.dmul)): + for idx, (peg_keeper, swap, dmul) in enumerate( + zip(self.peg_keepers, self.swaps, self.dmul) + ): balances_before = [swap.balances(i) for i in range(2)] profit = 0 @@ -32,18 +31,23 @@ def invariant_check_diff(self): with boa.env.prank(self.alice): profit = peg_keeper.update() except BoaError as e: - if 'peg unprofitable' in str(e): + if "peg unprofitable" in str(e): continue balances = [swap.balances(i) for i in range(2)] diff = balances[1] * 10**18 // dmul[1] - balances[0] * 10**18 // dmul[0] - last_diff = balances_before[1] * 10**18 // dmul[1] - balances_before[0] * 10**18 // dmul[0] + last_diff = ( + balances_before[1] * 10**18 // dmul[1] + - balances_before[0] * 10**18 // dmul[0] + ) if diff == last_diff: assert profit == 0 else: - assert (abs(diff - (last_diff - last_diff // 5)) <= 5) or (peg_keeper.debt() == 0) + assert (abs(diff - (last_diff - last_diff // 5)) <= 5) or ( + peg_keeper.debt() == 0 + ) def test_stable_peg( @@ -56,7 +60,9 @@ def test_stable_peg( receiver, admin, ): - StateMachine.TestCase.settings = settings(max_examples=20, stateful_step_count=40, suppress_health_check=HealthCheck.all()) + StateMachine.TestCase.settings = settings( + max_examples=20, stateful_step_count=40, suppress_health_check=HealthCheck.all() + ) for k, v in locals().items(): setattr(StateMachine, k, v) run_state_machine_as_test(StateMachine) diff --git a/tests/stableborrow/stabilize/stateful/test_profit.py b/tests/stableborrow/stabilize/stateful/test_profit.py index 48304bab..44927b88 100644 --- a/tests/stableborrow/stabilize/stateful/test_profit.py +++ b/tests/stableborrow/stabilize/stateful/test_profit.py @@ -7,10 +7,7 @@ from . import base -pytestmark = pytest.mark.usefixtures( - "add_initial_liquidity", - "mint_alice" -) +pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_alice") class StateMachine(base.StateMachine): @@ -39,16 +36,16 @@ def invariant_profit(self): with boa.env.prank(self.alice): peg_keeper.update() except BoaError as e: - if 'peg unprofitable' in str(e): + if "peg unprofitable" in str(e): continue debt = peg_keeper.debt() lp_balance = swap.balanceOf(peg_keeper) profit = peg_keeper.calc_profit() virtual_price = swap.get_virtual_price() - aim_profit = lp_balance - debt * 10 ** 18 // virtual_price + aim_profit = lp_balance - debt * 10**18 // virtual_price assert 2e18 > aim_profit - profit >= 0 - assert lp_balance * virtual_price - debt * 10 ** 18 >= 0 + assert lp_balance * virtual_price - debt * 10**18 >= 0 def test_profit( @@ -61,11 +58,15 @@ def test_profit( receiver, admin, ): - fee = 4 * 10 ** 7 + fee = 4 * 10**7 with boa.env.prank(admin): for swap in swaps: swap.eval(f"self.fee = {fee}") - StateMachine.TestCase.settings = settings(max_examples=100, stateful_step_count=40, suppress_health_check=HealthCheck.all()) + StateMachine.TestCase.settings = settings( + max_examples=100, + stateful_step_count=40, + suppress_health_check=HealthCheck.all(), + ) for k, v in locals().items(): setattr(StateMachine, k, v) run_state_machine_as_test(StateMachine) @@ -91,7 +92,7 @@ def test_unprofitable_peg( state.advance_time() state.invariant_profit_increases() state.invariant_profit() - state.add_one_coin(idx=0, pct=fee / 10 ** 10, pool_idx=0) + state.add_one_coin(idx=0, pct=fee / 10**10, pool_idx=0) state.advance_time() state.invariant_profit_increases() state.invariant_profit() diff --git a/tests/stableborrow/stabilize/stateful/test_stable_peg_caller_profit.py b/tests/stableborrow/stabilize/stateful/test_stable_peg_caller_profit.py index b0dbf593..35f6b47f 100644 --- a/tests/stableborrow/stabilize/stateful/test_stable_peg_caller_profit.py +++ b/tests/stableborrow/stabilize/stateful/test_stable_peg_caller_profit.py @@ -7,10 +7,7 @@ from . import base -pytestmark = pytest.mark.usefixtures( - "add_initial_liquidity", - "mint_alice" -) +pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_alice") class StateMachine(base.StateMachine): @@ -18,6 +15,7 @@ class StateMachine(base.StateMachine): Stateful test that performs a series of deposits, swaps and withdrawals and confirms that profit is calculated right. """ + @invariant() def invariant_expected_caller_profit(self): """ @@ -33,7 +31,7 @@ def invariant_expected_caller_profit(self): with boa.env.prank(self.alice): caller_profit = peg_keeper.update() except BoaError as e: - if 'peg unprofitable' in str(e): + if "peg unprofitable" in str(e): continue caller_balance = swap.balanceOf(self.alice) @@ -62,7 +60,11 @@ def test_stable_peg( for swap in swaps: swap.eval(f"self.fee = {4 * 10**7}") - StateMachine.TestCase.settings = settings(max_examples=100, stateful_step_count=40, suppress_health_check=HealthCheck.all()) + StateMachine.TestCase.settings = settings( + max_examples=100, + stateful_step_count=40, + suppress_health_check=HealthCheck.all(), + ) for k, v in locals().items(): setattr(StateMachine, k, v) run_state_machine_as_test(StateMachine) @@ -86,10 +88,14 @@ def test_expected_profit_amount( state = StateMachine() state.advance_time() state.invariant_expected_caller_profit() - state.add_coins(amount_0=0.4586551720385922, amount_1=0.2753979563491829, pool_idx=1) + state.add_coins( + amount_0=0.4586551720385922, amount_1=0.2753979563491829, pool_idx=1 + ) state.advance_time() state.invariant_expected_caller_profit() - state.remove_imbalance(amount_0=0.2708333333333333, amount_1=6.103515625e-05, pool_idx=1) + state.remove_imbalance( + amount_0=0.2708333333333333, amount_1=6.103515625e-05, pool_idx=1 + ) state.advance_time() state.invariant_expected_caller_profit() state.remove_one_coin(idx=0, pct=0.5, pool_idx=1) @@ -119,7 +125,7 @@ def test_expected_profit_amount_2( ): with boa.env.prank(admin): for swap in swaps: - swap.eval(f"self.fee = {4 * 10 ** 7}") + swap.eval(f"self.fee = {4 * 10**7}") for k, v in locals().items(): setattr(StateMachine, k, v) state = StateMachine() @@ -194,13 +200,15 @@ def test_calc_revert( ): with boa.env.prank(admin): for swap in swaps: - swap.eval(f"self.fee = {4 * 10 ** 7}") + swap.eval(f"self.fee = {4 * 10**7}") for k, v in locals().items(): setattr(StateMachine, k, v) state = StateMachine() state.advance_time() state.invariant_expected_caller_profit() - state.remove_imbalance(amount_0=0.3333333333333333, amount_1=0.39515566169219923, pool_idx=1) + state.remove_imbalance( + amount_0=0.3333333333333333, amount_1=0.39515566169219923, pool_idx=1 + ) state.advance_time() state.invariant_expected_caller_profit() state.add_one_coin(idx=0, pct=1e-05, pool_idx=1) @@ -218,7 +226,9 @@ def test_calc_revert( state.remove_imbalance(amount_0=1e-05, amount_1=0.3333333333333333, pool_idx=1) state.advance_time() state.invariant_expected_caller_profit() - state.remove_imbalance(amount_0=0.7435513040335519, amount_1=1.0537488943664356e-06, pool_idx=0) + state.remove_imbalance( + amount_0=0.7435513040335519, amount_1=1.0537488943664356e-06, pool_idx=0 + ) state.advance_time() state.invariant_expected_caller_profit() state.remove_imbalance(amount_0=1e-05, amount_1=1e-05, pool_idx=0) @@ -245,11 +255,15 @@ def test_calc_revert( state.remove_imbalance(amount_0=1e-06, amount_1=6.103515625e-05, pool_idx=1) state.advance_time() state.invariant_expected_caller_profit() - state.remove_imbalance(amount_0=1.0000000000000002e-06, amount_1=0.3333333333333333, pool_idx=1) + state.remove_imbalance( + amount_0=1.0000000000000002e-06, amount_1=0.3333333333333333, pool_idx=1 + ) state.advance_time() state.invariant_expected_caller_profit() state.add_one_coin(idx=0, pct=8.107951174795291e-06, pool_idx=0) state.advance_time() state.invariant_expected_caller_profit() - state.remove_imbalance(amount_0=6.103515625e-05, amount_1=0.5373502140513607, pool_idx=1) + state.remove_imbalance( + amount_0=6.103515625e-05, amount_1=0.5373502140513607, pool_idx=1 + ) state.teardown() diff --git a/tests/stableborrow/stabilize/stateful/test_withdraw_profit.py b/tests/stableborrow/stabilize/stateful/test_withdraw_profit.py index c90b7aca..d6cac6f5 100644 --- a/tests/stableborrow/stabilize/stateful/test_withdraw_profit.py +++ b/tests/stableborrow/stabilize/stateful/test_withdraw_profit.py @@ -5,10 +5,7 @@ from hypothesis import settings from hypothesis.stateful import run_state_machine_as_test, rule, invariant -pytestmark = pytest.mark.usefixtures( - "add_initial_liquidity", - "mint_alice" -) +pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "mint_alice") class StateMachine(base.StateMachine): @@ -45,7 +42,11 @@ def invariant_withdraw_profit(self): peg_keeper.withdraw_profit() debt = peg_keeper.debt() - amount = 5 * (debt + 1) + swap.balances(0) * 10**18 // dmul[0] - swap.balances(1) + amount = ( + 5 * (debt + 1) + + swap.balances(0) * 10**18 // dmul[0] + - swap.balances(1) + ) if amount < 0: return StateMachine._mint(self.alice, [self.stablecoin], [amount]) @@ -64,8 +65,9 @@ def invariant_withdraw_profit(self): continue assert peg_keeper.debt() == 0 - assert abs(swap.balances(0) * 10 ** 18 // dmul[0] - (swap.balances(1) - 4 * debt)) <= \ - debt * swap.fee() // (2 * 10 ** 10) + assert abs( + swap.balances(0) * 10**18 // dmul[0] - (swap.balances(1) - 4 * debt) + ) <= debt * swap.fee() // (2 * 10**10) @pytest.mark.parametrize("always_withdraw", [False, True]) @@ -79,11 +81,11 @@ def test_withdraw_profit( receiver, admin, always_withdraw, - _mint + _mint, ): with boa.env.prank(admin): for swap in swaps: - swap.eval(f"self.fee = {4 * 10 ** 7}") + swap.eval(f"self.fee = {4 * 10**7}") StateMachine.TestCase.settings = settings(max_examples=20, stateful_step_count=40) for k, v in locals().items(): @@ -100,13 +102,13 @@ def test_withdraw_profit_example_1( alice, receiver, admin, - _mint + _mint, ): always_withdraw = False with boa.env.prank(admin): for swap in swaps: - swap.eval(f"self.fee = {4 * 10 ** 7}") + swap.eval(f"self.fee = {4 * 10**7}") StateMachine.TestCase.settings = settings(max_examples=20, stateful_step_count=40) for k, v in locals().items(): @@ -117,7 +119,9 @@ def test_withdraw_profit_example_1( state.add_one_coin(idx=0, pct=1e-06, pool_idx=0) state.advance_time() state.invariant_withdraw_profit() - state.add_coins(amount_0=0.4500005000000001, amount_1=0.3333333333333333, pool_idx=1) + state.add_coins( + amount_0=0.4500005000000001, amount_1=0.3333333333333333, pool_idx=1 + ) state.advance_time() state.invariant_withdraw_profit() state.add_coins(amount_0=0.03125, amount_1=1e-06, pool_idx=0) @@ -150,13 +154,13 @@ def test_withdraw_profit_example_2( alice, receiver, admin, - _mint + _mint, ): always_withdraw = True with boa.env.prank(admin): for swap in swaps: - swap.eval(f"self.fee = {4 * 10 ** 7}") + swap.eval(f"self.fee = {4 * 10**7}") StateMachine.TestCase.settings = settings(max_examples=20, stateful_step_count=40) for k, v in locals().items(): @@ -170,10 +174,14 @@ def test_withdraw_profit_example_2( state.remove_imbalance(amount_0=0.5202005534658294, amount_1=1e-05, pool_idx=0) state.advance_time() state.invariant_withdraw_profit() - state.remove_imbalance(amount_0=1.0000000000000002e-06, amount_1=6.103515625e-05, pool_idx=1) + state.remove_imbalance( + amount_0=1.0000000000000002e-06, amount_1=6.103515625e-05, pool_idx=1 + ) state.advance_time() state.invariant_withdraw_profit() - state.remove_imbalance(amount_0=1.0000000000000002e-06, amount_1=0.4592796580617876, pool_idx=1) + state.remove_imbalance( + amount_0=1.0000000000000002e-06, amount_1=0.4592796580617876, pool_idx=1 + ) state.advance_time() state.invariant_withdraw_profit() state.exchange(idx=0, pct=1e-05, pool_idx=1) @@ -185,10 +193,14 @@ def test_withdraw_profit_example_2( state.remove_imbalance(amount_0=0.5, amount_1=0.3333333333333333, pool_idx=0) state.advance_time() state.invariant_withdraw_profit() - state.remove_imbalance(amount_0=0.8999999999999999, amount_1=0.5662503294199993, pool_idx=1) + state.remove_imbalance( + amount_0=0.8999999999999999, amount_1=0.5662503294199993, pool_idx=1 + ) state.advance_time() state.invariant_withdraw_profit() - state.remove_imbalance(amount_0=0.19315130644085848, amount_1=0.3333333333333333, pool_idx=0) + state.remove_imbalance( + amount_0=0.19315130644085848, amount_1=0.3333333333333333, pool_idx=0 + ) state.advance_time() state.invariant_withdraw_profit() state.exchange(idx=0, pct=6.103515625e-05, pool_idx=1) @@ -209,7 +221,9 @@ def test_withdraw_profit_example_2( state.remove_imbalance(amount_0=6.103515625e-05, amount_1=1e-06, pool_idx=0) state.advance_time() state.invariant_withdraw_profit() - state.remove_imbalance(amount_0=0.6802762567260562, amount_1=0.4585453088794063, pool_idx=0) + state.remove_imbalance( + amount_0=0.6802762567260562, amount_1=0.4585453088794063, pool_idx=0 + ) state.advance_time() state.invariant_withdraw_profit() state.exchange(idx=1, pct=0.5111175346916537, pool_idx=1) diff --git a/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py b/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py index d1faf531..a75416a9 100644 --- a/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py +++ b/tests/stableborrow/stabilize/unitary/test_agg_monetary_policy_3.py @@ -1,17 +1,23 @@ import boa import pytest from collections import defaultdict +from tests.utils.deployers import ( + MOCK_FACTORY_DEPLOYER, + MOCK_MARKET_DEPLOYER, + MOCK_PEG_KEEPER_DEPLOYER, + AGG_MONETARY_POLICY3_DEPLOYER, +) +from tests.utils.constants import ZERO_ADDRESS RATE0 = 634195839 # 2% -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" @pytest.fixture(scope="module") def mock_factory(admin): with boa.env.prank(admin): - factory = boa.load('contracts/testing/MockFactory.vy') + factory = MOCK_FACTORY_DEPLOYER.deploy() for i in range(3): - market = boa.load('contracts/testing/MockMarket.vy') + market = MOCK_MARKET_DEPLOYER.deploy() factory.add_market(market.address, 10**6 * 10**18) return factory @@ -21,26 +27,26 @@ def mock_peg_keepers(admin, stablecoin): with boa.env.prank(admin): pks = [] for i in range(4): - pk = boa.load('contracts/testing/MockPegKeeper.vy', 10 ** 18, stablecoin) + pk = MOCK_PEG_KEEPER_DEPLOYER.deploy(10**18, stablecoin) pk.set_debt(10**4 * 10**18) pks.append(pk) return pks -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def mp(mock_factory, mock_peg_keepers, price_oracle, admin): with boa.env.prank(admin): price_oracle.set_price(10**18) - return boa.load( - 'contracts/mpolicies/AggMonetaryPolicy3.vy', + return AGG_MONETARY_POLICY3_DEPLOYER.deploy( admin, price_oracle.address, mock_factory.address, [p.address for p in mock_peg_keepers] + [ZERO_ADDRESS], RATE0, 2 * 10**16, # Sigma 2% - 5 * 10**16) # Target debt fraction 5% + 5 * 10**16, + ) # Target debt fraction 5% def test_broken_markets(mp, mock_factory, admin): @@ -78,9 +84,13 @@ def test_candles(mp, mock_factory, admin): controller = controllers[t % 3] new_debt = t * 10**5 * 10**18 mock_factory.set_debt(controller, new_debt) - d_total_0, d_for_0 = mp.internal.read_debt(controller, True) + d_total_0, d_for_0 = mp.eval( + f"self.read_debt({controller}, True)", return_type="(uint256, uint256)" + ) mp.rate_write(controller) - d_total_1, d_for_1 = mp.internal.read_debt(controller, False) + d_total_1, d_for_1 = mp.eval( + f"self.read_debt({controller}, False)", return_type="(uint256, uint256)" + ) current_total = mock_factory.total_debt() assert d_total_0 == d_total_1 <= current_total assert d_for_0 == d_for_1 @@ -105,16 +115,20 @@ def test_add_controllers(mp, mock_factory, admin): additional_ceilings = [10**7, 10**8, 10**9] added_debt = 0 - initial_debt, _ = mp.internal.get_total_debt(ZERO_ADDRESS) + initial_debt, _ = mp.eval( + f"self.get_total_debt({ZERO_ADDRESS})", return_type="(uint256, uint256)" + ) with boa.env.prank(admin): for ceiling, debt in zip(additional_ceilings, additional_debts): - market = boa.load('contracts/testing/MockMarket.vy') + market = MOCK_MARKET_DEPLOYER.deploy() mock_factory.add_market(market.address, ceiling) mp.rate_write() controller = mock_factory.controllers(mock_factory.n_collaterals() - 1) added_debt += debt mock_factory.set_debt(controller, debt) - total_debt, debt_for = mp.internal.get_total_debt(controller) + total_debt, debt_for = mp.eval( + f"self.get_total_debt({controller})", return_type="(uint256, uint256)" + ) assert total_debt == initial_debt + added_debt assert debt_for == debt diff --git a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py index 1a45211c..85bc32f9 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_admin_functions.py @@ -1,7 +1,7 @@ import boa +from tests.utils.constants import ZERO_ADDRESS -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" ADMIN_ACTIONS_DEADLINE = 3 * 86400 @@ -17,10 +17,13 @@ def test_parameters(peg_keepers, swaps, stablecoin, admin, reg): assert peg_keeper.regulator() == reg.address -def test_update_access(peg_keepers, peg_keeper_updater, - add_initial_liquidity, - provide_token_to_peg_keepers, - imbalance_pools): +def test_update_access( + peg_keepers, + peg_keeper_updater, + add_initial_liquidity, + provide_token_to_peg_keepers, + imbalance_pools, +): imbalance_pools(1) with boa.env.prank(peg_keeper_updater): for pk in peg_keepers: @@ -73,7 +76,7 @@ def test_new_admin(peg_keepers, admin, alice, bob): assert pk.admin() == admin assert pk.future_admin() == alice - assert boa.env.vm.patch.timestamp + ADMIN_ACTIONS_DEADLINE == pk.new_admin_deadline() + assert boa.env.timestamp + ADMIN_ACTIONS_DEADLINE == pk.new_admin_deadline() # apply_new_admin boa.env.time_travel(ADMIN_ACTIONS_DEADLINE - 60) diff --git a/tests/stableborrow/stabilize/unitary/test_pk_delay.py b/tests/stableborrow/stabilize/unitary/test_pk_delay.py index 3c7eedaa..2cb99454 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_delay.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_delay.py @@ -5,11 +5,15 @@ ACTION_DELAY = 12 -pytestmark = pytest.mark.usefixtures("add_initial_liquidity", "provide_token_to_peg_keepers", "mint_bob") +pytestmark = pytest.mark.usefixtures( + "add_initial_liquidity", "provide_token_to_peg_keepers", "mint_bob" +) @pytest.mark.parametrize("method", ["provide", "withdraw"]) -def test_update_delay(peg_keepers, swaps, redeemable_tokens, stablecoin, bob, peg_keeper_updater, method): +def test_update_delay( + peg_keepers, swaps, redeemable_tokens, stablecoin, bob, peg_keeper_updater, method +): for pk, swap, rtoken in zip(peg_keepers, swaps, redeemable_tokens): with boa.env.anchor(): with boa.env.prank(bob): @@ -26,7 +30,9 @@ def test_update_delay(peg_keepers, swaps, redeemable_tokens, stablecoin, bob, pe @pytest.mark.parametrize("method", ["provide", "withdraw"]) -def test_update_no_delay(peg_keepers, swaps, redeemable_tokens, stablecoin, bob, peg_keeper_updater, method): +def test_update_no_delay( + peg_keepers, swaps, redeemable_tokens, stablecoin, bob, peg_keeper_updater, method +): for pk, swap, rtoken in zip(peg_keepers, swaps, redeemable_tokens): with boa.env.anchor(): with boa.env.prank(bob): @@ -36,7 +42,7 @@ def test_update_no_delay(peg_keepers, swaps, redeemable_tokens, stablecoin, bob, swap.add_liquidity([0, stablecoin.balanceOf(bob)], 0) t0 = pk.last_change() - boa.env.vm.patch.timestamp = t0 + ACTION_DELAY - 3 + boa.env.timestamp = t0 + ACTION_DELAY - 3 with boa.env.prank(peg_keeper_updater): pk.update() assert pk.last_change() == t0 diff --git a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py index 0a9beacd..730ab673 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_offboarding.py @@ -1,6 +1,8 @@ import boa import pytest +from tests.utils.deployers import PEG_KEEPER_OFFBOARDING_DEPLOYER + pytestmark = pytest.mark.usefixtures( "add_initial_liquidity", "provide_token_to_peg_keepers", @@ -13,21 +15,27 @@ @pytest.fixture(scope="module") -def offboarding(stablecoin, receiver, admin, peg_keepers): - hr = boa.load( - 'contracts/stabilizer/PegKeeperOffboarding.vy', - stablecoin, ZERO_ADDRESS, receiver, admin, admin - ) +def offboarding(receiver, admin, peg_keepers): + hr = PEG_KEEPER_OFFBOARDING_DEPLOYER.deploy(receiver, admin, admin) with boa.env.prank(admin): for peg_keeper in peg_keepers: peg_keeper.set_new_regulator(hr) return hr -def test_offboarding(offboarding, stablecoin, peg_keepers, swaps, receiver, admin, alice, peg_keeper_updater): +def test_offboarding( + offboarding, + stablecoin, + peg_keepers, + swaps, + receiver, + admin, + alice, + peg_keeper_updater, +): with boa.env.prank(admin): for peg_keeper in peg_keepers: - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10**18}") for peg_keeper, swap in zip(peg_keepers, swaps): assert offboarding.provide_allowed(peg_keeper) == 0 @@ -48,7 +56,7 @@ def test_offboarding(offboarding, stablecoin, peg_keepers, swaps, receiver, admi def test_set_killed(offboarding, peg_keepers, admin, stablecoin): peg_keeper = peg_keepers[0] - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10**18}") with boa.env.prank(admin): assert offboarding.is_killed() == 0 @@ -59,7 +67,7 @@ def test_set_killed(offboarding, peg_keepers, admin, stablecoin): assert offboarding.is_killed() == 1 assert offboarding.provide_allowed(peg_keeper) == 0 - assert offboarding.withdraw_allowed(peg_keeper) == 2 ** 256 - 1 + assert offboarding.withdraw_allowed(peg_keeper) == 2**256 - 1 offboarding.set_killed(2) assert offboarding.is_killed() == 2 @@ -108,5 +116,3 @@ def test_admin(reg, admin, alice, agg, receiver): reg.set_admin(alice) assert reg.admin() == alice - - diff --git a/tests/stableborrow/stabilize/unitary/test_pk_profit.py b/tests/stableborrow/stabilize/unitary/test_pk_profit.py index 17703123..1286df9f 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_profit.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_profit.py @@ -17,14 +17,14 @@ def _inner(amount, i=None): if i is not None: if j != i: continue - exchange_amount = amount * 5 // 10**(18 - rtoken.decimals()) + exchange_amount = amount * 5 // 10 ** (18 - rtoken.decimals()) if exchange_amount == 0: continue set_fee(swap, 10**9) with boa.env.prank(alice): - rtoken._mint_for_testing(alice, exchange_amount) + boa.deal(rtoken, alice, exchange_amount) rtoken.approve(swap.address, exchange_amount) out = swap.exchange(0, 1, exchange_amount, 0) @@ -46,7 +46,9 @@ def test_calc_initial_profit(peg_keepers, swaps): for peg_keeper, swap in zip(peg_keepers, swaps): debt = peg_keeper.debt() assert debt / swap.get_virtual_price() < swap.balanceOf(peg_keeper) - aim_profit = swap.balanceOf(peg_keeper) - debt * 10**18 // swap.get_virtual_price() + aim_profit = ( + swap.balanceOf(peg_keeper) - debt * 10**18 // swap.get_virtual_price() + ) assert aim_profit >= peg_keeper.calc_profit() > 0 @@ -77,11 +79,13 @@ def test_withdraw_profit( alice, peg_keeper_updater, donate_fee, - price_aggregator + price_aggregator, ): """Withdraw profit and update for the whole debt.""" - for i, (peg_keeper, swap, rtoken) in enumerate(zip(peg_keepers, swaps, redeemable_tokens)): + for i, (peg_keeper, swap, rtoken) in enumerate( + zip(peg_keepers, swaps, redeemable_tokens) + ): with boa.env.anchor(): make_profit(donate_fee, i) rtoken_mul = 10 ** (18 - rtoken.decimals()) @@ -107,7 +111,10 @@ def test_withdraw_profit( diff = 5 * debt - assert swap.balances(0) + (diff - diff // 5) // rtoken_mul == swap.balances(1) // rtoken_mul + assert ( + swap.balances(0) + (diff - diff // 5) // rtoken_mul + == swap.balances(1) // rtoken_mul + ) # Not checking balances==balanceOf because admin fee is nonzero @@ -137,7 +144,16 @@ def test_profit_receiver( assert swap.balanceOf(receiver) > 0 -def test_unprofitable_peg(swaps, peg_keepers, redeemable_tokens, stablecoin, alice, imbalance_pool, admin, set_fee): +def test_unprofitable_peg( + swaps, + peg_keepers, + redeemable_tokens, + stablecoin, + alice, + imbalance_pool, + admin, + set_fee, +): for swap, peg_keeper, rtoken in zip(swaps, peg_keepers, redeemable_tokens): with boa.env.anchor(): # Leave a little of debt @@ -154,7 +170,7 @@ def test_unprofitable_peg(swaps, peg_keepers, redeemable_tokens, stablecoin, ali set_fee(swap, 5 * 10**9) boa.env.time_travel(12) - with boa.reverts('peg unprofitable'): # dev: peg was unprofitable + with boa.reverts("peg unprofitable"): # dev: peg was unprofitable with boa.env.prank(alice): peg_keeper.update() diff --git a/tests/stableborrow/stabilize/unitary/test_pk_provide.py b/tests/stableborrow/stabilize/unitary/test_pk_provide.py index 4374decc..36edd5aa 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_provide.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_provide.py @@ -11,13 +11,7 @@ @given(amount=st.integers(min_value=10**20, max_value=10**24)) def test_provide( - swaps, - redeemable_tokens, - stablecoin, - alice, - amount, - peg_keepers, - peg_keeper_updater + swaps, redeemable_tokens, stablecoin, alice, amount, peg_keepers, peg_keeper_updater ): for swap, rtoken, peg_keeper in zip(swaps, redeemable_tokens, peg_keepers): rtoken_mul = 10 ** (18 - rtoken.decimals()) @@ -31,10 +25,14 @@ def test_provide( new_balances = [swap.balances(0), swap.balances(1)] assert new_balances[0] == balances[0] - assert (new_balances[1]) // rtoken_mul == (balances[1] + amount // 5) // rtoken_mul + assert ( + (new_balances[1]) // rtoken_mul == (balances[1] + amount // 5) // rtoken_mul + ) -def test_min_coin_amount(swaps, initial_amounts, alice, peg_keepers, peg_keeper_updater): +def test_min_coin_amount( + swaps, initial_amounts, alice, peg_keepers, peg_keeper_updater +): for swap, peg_keeper, initial in zip(swaps, peg_keepers, initial_amounts): with boa.env.prank(alice): swap.add_liquidity([initial[0], 0], 0) @@ -42,14 +40,17 @@ def test_min_coin_amount(swaps, initial_amounts, alice, peg_keepers, peg_keeper_ assert peg_keeper.update() -def test_almost_balanced(swaps, alice, peg_keepers, peg_keeper_updater, redeemable_tokens, stablecoin): +def test_almost_balanced( + swaps, alice, peg_keepers, peg_keeper_updater, redeemable_tokens, stablecoin +): for swap, peg_keeper, rtoken in zip(swaps, peg_keepers, redeemable_tokens): with boa.env.prank(alice): - diff = swap.balances(1) * 10 ** (18 - stablecoin.decimals()) -\ - swap.balances(0) * 10 ** (18 - rtoken.decimals()) + diff = swap.balances(1) * 10 ** ( + 18 - stablecoin.decimals() + ) - swap.balances(0) * 10 ** (18 - rtoken.decimals()) amounts = [diff if diff > 0 else 0, -diff if diff < 0 else 0] amounts[0] += 10 swap.add_liquidity(amounts, 0) - with boa.reverts('peg unprofitable'): # dev: peg was unprofitable + with boa.reverts("peg unprofitable"): # dev: peg was unprofitable with boa.env.prank(peg_keeper_updater): peg_keeper.update() diff --git a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py index ef932b62..dedd45a0 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_regulator.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_regulator.py @@ -1,17 +1,16 @@ import boa import pytest from hypothesis import strategies as st, given +from tests.utils.deployers import MOCK_PEG_KEEPER_DEPLOYER - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" ADMIN_ACTIONS_DEADLINE = 3 * 86400 def test_price_range(peg_keepers, swaps, stablecoin, admin, receiver, reg, rate_oracle): with boa.env.prank(admin): - reg.set_price_deviation(10 ** 17) + reg.set_price_deviation(10**17) for peg_keeper in peg_keepers: - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10**18}") for peg_keeper, swap in zip(peg_keepers, swaps): assert reg.provide_allowed(peg_keeper) @@ -21,7 +20,7 @@ def test_price_range(peg_keepers, swaps, stablecoin, admin, receiver, reg, rate_ try: swap.eval("self.rate_multipliers[0] *= 2") except: - rate_oracle.set(1, 2 * 10 ** 18) + rate_oracle.set(1, 2 * 10**18) assert reg.provide_allowed(peg_keeper) assert reg.withdraw_allowed(peg_keeper) @@ -29,18 +28,31 @@ def test_price_range(peg_keepers, swaps, stablecoin, admin, receiver, reg, rate_ try: swap.eval("self.rate_multipliers[0] *= 5") except: - rate_oracle.set(1, 10 ** 19) + rate_oracle.set(1, 10**19) assert not reg.provide_allowed(peg_keeper) assert not reg.withdraw_allowed(peg_keeper) -def test_price_order(peg_keepers, mock_peg_keepers, swaps, initial_amounts, stablecoin, admin, alice, mint_alice, reg, agg): +def test_price_order( + peg_keepers, + mock_peg_keepers, + swaps, + initial_amounts, + stablecoin, + admin, + alice, + mint_alice, + reg, + agg, +): with boa.env.prank(admin): reg.remove_peg_keepers([mock.address for mock in mock_peg_keepers]) # note: assuming swaps' prices are close enough - for i, (peg_keeper, swap, (initial_amount, _)) in enumerate(zip(peg_keepers, swaps, initial_amounts)): + for i, (peg_keeper, swap, (initial_amount, _)) in enumerate( + zip(peg_keepers, swaps, initial_amounts) + ): with boa.env.anchor(): with boa.env.prank(admin): # Price change break aggregator.price() check @@ -62,13 +74,13 @@ def test_price_order(peg_keepers, mock_peg_keepers, swaps, initial_amounts, stab def test_aggregator_price(peg_keepers, mock_peg_keepers, reg, agg, admin, stablecoin): - mock_peg_keeper = boa.load('contracts/testing/MockPegKeeper.vy', 10 ** 18, stablecoin) + mock_peg_keeper = MOCK_PEG_KEEPER_DEPLOYER.deploy(10**18, stablecoin) for peg_keeper in peg_keepers: - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10**18}") with boa.env.prank(admin): agg.add_price_pair(mock_peg_keeper) for price in [0.95, 1.05]: - mock_peg_keeper.set_price(int(price * 10 ** 18)) + mock_peg_keeper.set_price(int(price * 10**18)) boa.env.time_travel(seconds=50000) for peg_keeper in peg_keepers: assert (reg.provide_allowed(peg_keeper) > 0) == (price > 1) @@ -76,58 +88,60 @@ def test_aggregator_price(peg_keepers, mock_peg_keepers, reg, agg, admin, stable def test_debt_limit(peg_keepers, mock_peg_keepers, reg, agg, admin, stablecoin): - alpha, beta = 10 ** 18 // 2, 10 ** 18 // 4 + alpha, beta = 10**18 // 2, 10**18 // 4 with boa.env.prank(admin): reg.set_debt_parameters(alpha, beta) for mock in mock_peg_keepers: - mock.set_price(10 ** 18) + mock.set_price(10**18) all_pks = mock_peg_keepers + peg_keepers # First peg keeper debt limit for pk in all_pks: pk.eval("self.debt = 0") - stablecoin.eval(f"self.balanceOf[{pk.address}] = {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{pk.address}] = {10**18}") for pk in all_pks: - assert reg.provide_allowed(pk.address) == alpha ** 2 // 10 ** 18 + assert reg.provide_allowed(pk.address) == alpha**2 // 10**18 # Three peg keepers debt limits for pk in all_pks[:2]: pk.eval("self.debt = 10 ** 18") stablecoin.eval(f"self.balanceOf[{pk.address}] = 0") for pk in all_pks[2:]: - assert reg.provide_allowed(pk.address) == pytest.approx(10 ** 18, abs=5) + assert reg.provide_allowed(pk.address) == pytest.approx(10**18, abs=5) @given( - a=st.integers(min_value=0, max_value=10 ** 18), - b=st.integers(min_value=0, max_value=10 ** 18 // 2), - debts=st.lists(st.integers(min_value=0, max_value=10 ** 18), min_size=3, max_size=3), + a=st.integers(min_value=0, max_value=10**18), + b=st.integers(min_value=0, max_value=10**18 // 2), + debts=st.lists(st.integers(min_value=0, max_value=10**18), min_size=3, max_size=3), ) -def test_debt_limit_formula(peg_keepers, mock_peg_keepers, reg, admin, stablecoin, a, b, debts): +def test_debt_limit_formula( + peg_keepers, mock_peg_keepers, reg, admin, stablecoin, a, b, debts +): def sqrt(_debt): - return int((_debt * 10 ** 18) ** .5) + return int((_debt * 10**18) ** 0.5) - a = min(a, 10 ** 18 - b) - alpha, beta = a * 10 ** 18 // (10 ** 18 - b), b * 10 ** 18 // (10 ** 18 - b) + a = min(a, 10**18 - b) + alpha, beta = a * 10**18 // (10**18 - b), b * 10**18 // (10**18 - b) with boa.env.prank(admin): reg.set_debt_parameters(alpha, beta) for peg_keeper, debt in zip((mock_peg_keepers + peg_keepers)[:3], debts): peg_keeper.eval(f"self.debt = {debt}") - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] = {10 ** 18 - debt}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] = {10**18 - debt}") peg_keeper = peg_keepers[-1] - peg_keeper.eval(f"self.debt = 0") - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] = {10 ** 18}") + peg_keeper.eval("self.debt = 0") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] = {10**18}") sqrt_new_debt = sqrt(reg.provide_allowed(peg_keeper)) assert sqrt_new_debt == pytest.approx( - a + b * sum([sqrt(debt) for debt in debts] + [sqrt_new_debt]) // 10 ** 18, - abs=10 ** 9, + a + b * sum([sqrt(debt) for debt in debts] + [sqrt_new_debt]) // 10**18, + abs=10**9, ) def test_set_killed(reg, peg_keepers, admin, stablecoin): peg_keeper = peg_keepers[0] - stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10 ** 18}") + stablecoin.eval(f"self.balanceOf[{peg_keeper.address}] += {10**18}") with boa.env.prank(admin): assert reg.is_killed() == 0 @@ -156,8 +170,8 @@ def test_set_killed(reg, peg_keepers, admin, stablecoin): def test_admin(reg, admin, alice, agg, receiver): # initial parameters assert reg.worst_price_threshold() == 3 * 10 ** (18 - 4) - assert reg.price_deviation() == 100 * 10 ** 18 - assert (reg.alpha(), reg.beta()) == (10 ** 18, 10 ** 18) + assert reg.price_deviation() == 100 * 10**18 + assert (reg.alpha(), reg.beta()) == (10**18, 10**18) assert reg.aggregator() == agg.address assert reg.fee_receiver() == receiver assert reg.emergency_admin() == admin @@ -169,9 +183,9 @@ def test_admin(reg, admin, alice, agg, receiver): with boa.reverts(): reg.set_worst_price_threshold(10 ** (18 - 3)) with boa.reverts(): - reg.set_price_deviation(10 ** 17) + reg.set_price_deviation(10**17) with boa.reverts(): - reg.set_debt_parameters(10 ** 18 // 2, 10 ** 18 // 5) + reg.set_debt_parameters(10**18 // 2, 10**18 // 5) with boa.reverts(): reg.set_aggregator(alice) with boa.reverts(): @@ -188,11 +202,11 @@ def test_admin(reg, admin, alice, agg, receiver): reg.set_worst_price_threshold(10 ** (18 - 3)) assert reg.worst_price_threshold() == 10 ** (18 - 3) - reg.set_price_deviation(10 ** 17) - assert reg.price_deviation() == 10 ** 17 + reg.set_price_deviation(10**17) + assert reg.price_deviation() == 10**17 - reg.set_debt_parameters(10 ** 18 // 2, 10 ** 18 // 5) - assert (reg.alpha(), reg.beta()) == (10 ** 18 // 2, 10 ** 18 // 5) + reg.set_debt_parameters(10**18 // 2, 10**18 // 5) + assert (reg.alpha(), reg.beta()) == (10**18 // 2, 10**18 // 5) reg.set_aggregator(alice) assert reg.aggregator() == alice @@ -216,7 +230,8 @@ def test_admin(reg, admin, alice, agg, receiver): def get_peg_keepers(reg): return [ # pk.get("peg_keeper") for pk in reg._storage.peg_keepers.get() Available for titanoboa >= 0.1.8 - reg.peg_keepers(i)[0] for i in range(reg.eval("len(self.peg_keepers)")) + reg.peg_keepers(i)[0] + for i in range(reg.eval("len(self.peg_keepers)", return_type="uint256")) ] @@ -225,7 +240,8 @@ def preset_peg_keepers(reg, admin, stablecoin): with boa.env.prank(admin): reg.remove_peg_keepers(get_peg_keepers(reg)) return [ - boa.load('contracts/testing/MockPegKeeper.vy', (1 + i) * 10 ** 18, stablecoin).address for i in range(8) + MOCK_PEG_KEEPER_DEPLOYER.deploy((1 + i) * 10**18, stablecoin).address + for i in range(8) ] @@ -245,7 +261,9 @@ def test_add_peg_keepers(reg, admin, preset_peg_keepers, i, j): @given( i=st.integers(min_value=1, max_value=8), - js=st.lists(st.integers(min_value=0, max_value=7), min_size=1, max_size=8, unique=True), + js=st.lists( + st.integers(min_value=0, max_value=7), min_size=1, max_size=8, unique=True + ), ) def test_remove_peg_keepers(reg, admin, preset_peg_keepers, i, js): i = max(i, max(js) + 1) @@ -255,7 +273,9 @@ def test_remove_peg_keepers(reg, admin, preset_peg_keepers, i, js): to_remove = [preset_peg_keepers[j] for j in js] reg.remove_peg_keepers(to_remove) - assert set(get_peg_keepers(reg)) == set([preset_peg_keepers[k] for k in range(i) if k not in js]) + assert set(get_peg_keepers(reg)) == set( + [preset_peg_keepers[k] for k in range(i) if k not in js] + ) def test_peg_keepers_bad_values(reg, admin, preset_peg_keepers): diff --git a/tests/stableborrow/stabilize/unitary/test_pk_withdraw.py b/tests/stableborrow/stabilize/unitary/test_pk_withdraw.py index 993aeda2..b154c98b 100644 --- a/tests/stableborrow/stabilize/unitary/test_pk_withdraw.py +++ b/tests/stableborrow/stabilize/unitary/test_pk_withdraw.py @@ -64,9 +64,11 @@ def test_withdraw_dust_debt( redeemable_tokens, peg_keepers, peg_keeper_updater, - _mint + _mint, ): - for swap, peg_keeper, initial, rtoken in zip(swaps, peg_keepers, initial_amounts, redeemable_tokens): + for swap, peg_keeper, initial, rtoken in zip( + swaps, peg_keepers, initial_amounts, redeemable_tokens + ): rtoken_mul = 10 ** (18 - rtoken.decimals()) amount = 5 * (initial[1] - 1) _mint(alice, [stablecoin], [2 * amount]) @@ -77,7 +79,9 @@ def test_withdraw_dust_debt( with boa.env.prank(peg_keeper_updater): assert peg_keeper.update() - assert (swap.balances(1) - (amount - amount // 5)) // rtoken_mul == swap.balances(0) + assert ( + swap.balances(1) - (amount - amount // 5) + ) // rtoken_mul == swap.balances(0) remove_amount = swap.balances(1) - swap.balances(0) * rtoken_mul with boa.env.prank(alice): diff --git a/tests/stableborrow/stabilize/unitary/test_price_aggregation.py b/tests/stableborrow/stabilize/unitary/test_price_aggregation.py index 807844ef..3da6c3f8 100644 --- a/tests/stableborrow/stabilize/unitary/test_price_aggregation.py +++ b/tests/stableborrow/stabilize/unitary/test_price_aggregation.py @@ -1,12 +1,12 @@ -from ....conftest import approx import boa +import pytest def test_price_aggregator(stableswap_a, stableswap_b, stablecoin_a, agg, admin): amount = 300_000 * 10**6 dt = 86400 - assert approx(agg.price(), 10**18, 1e-6) + assert agg.price() == pytest.approx(10**18, rel=1e-6) assert agg.price_pairs(0)[0].lower() == stableswap_a.address.lower() assert agg.price_pairs(1)[0].lower() == stableswap_b.address.lower() @@ -15,7 +15,7 @@ def test_price_aggregator(stableswap_a, stableswap_b, stablecoin_a, agg, admin): agg.eval(f"self.last_tvl[{i}] = self.price_pairs[{i}].pool.totalSupply()") with boa.env.anchor(): with boa.env.prank(admin): - stablecoin_a._mint_for_testing(admin, amount) + boa.deal(stablecoin_a, admin, amount) stableswap_a.exchange(0, 1, amount, 0) p = stableswap_a.get_p() assert p > 10**18 * 1.01 @@ -23,10 +23,10 @@ def test_price_aggregator(stableswap_a, stableswap_b, stablecoin_a, agg, admin): boa.env.time_travel(dt) p_o = stableswap_a.price_oracle() - assert approx(p_o, p, 1e-4) + assert p_o == pytest.approx(p, rel=1e-4) # Two coins => agg price is average of the two - assert approx(agg.price(), (p_o + 10**18) / 2, 1e-3) + assert agg.price() == pytest.approx((p_o + 10**18) / 2, rel=1e-3) def test_crypto_agg(dummy_tricrypto, crypto_agg, stableswap_a, stablecoin_a, admin): @@ -47,10 +47,10 @@ def test_crypto_agg(dummy_tricrypto, crypto_agg, stableswap_a, stablecoin_a, adm boa.env.time_travel(200_000) p = crypto_agg.price() - assert approx(p, 1000 * 10**18, 1e-10) + assert p == pytest.approx(1000 * 10**18, rel=1e-10) amount = 300_000 * 10**6 - stablecoin_a._mint_for_testing(admin, amount) + boa.deal(stablecoin_a, admin, amount) stableswap_a.exchange(0, 1, amount, 0) boa.env.time_travel(200_000) diff --git a/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py b/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py index b16520c3..49f8066d 100644 --- a/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py +++ b/tests/stableborrow/stabilize/unitary/test_price_aggregation_with_chainlink.py @@ -1,4 +1,3 @@ -from ....conftest import approx import boa import pytest @@ -58,14 +57,13 @@ def true_raw_price(internal_raw_price, external_oracle_price): boa.env.time_travel(200_000) p = crypto_agg_with_external_oracle.price() - assert approx( - p, + assert p == pytest.approx( true_raw_price(internal_price_2, external_oracle_price) * 10**18, - 1e-10, + rel=1e-10, ) amount = 300_000 * 10**6 - stablecoin_a._mint_for_testing(admin, amount) + boa.deal(stablecoin_a, admin, amount) stableswap_a.exchange(0, 1, amount, 0) boa.env.time_travel(200_000) diff --git a/tests/stableborrow/test_approval.py b/tests/stableborrow/test_approval.py index 58bddf72..111c5009 100644 --- a/tests/stableborrow/test_approval.py +++ b/tests/stableborrow/test_approval.py @@ -10,11 +10,19 @@ def existing_loan(collateral_token, market_controller, accounts): n = 5 with boa.env.prank(user): - collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) market_controller.create_loan(c_amount, l_amount, n) -def test_create_loan(controller_factory, stablecoin, collateral_token, market_controller, market_amm, monetary_policy, accounts): +def test_create_loan( + controller_factory, + stablecoin, + collateral_token, + market_controller, + market_amm, + monetary_policy, + accounts, +): user = accounts[0] someone_else = accounts[1] @@ -24,10 +32,10 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co with boa.env.prank(user): with boa.env.anchor(): - collateral_token._mint_for_testing(user, initial_amount) + boa.deal(collateral_token, user, initial_amount) market_controller.create_loan(c_amount, l_amount, 5) - collateral_token._mint_for_testing(someone_else, initial_amount) + boa.deal(collateral_token, someone_else, initial_amount) with boa.env.anchor(): with boa.env.prank(someone_else): @@ -44,7 +52,9 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co market_controller.create_loan(c_amount, l_amount, 5, user) -def test_repay_all(stablecoin, collateral_token, market_controller, existing_loan, accounts): +def test_repay_all( + stablecoin, collateral_token, market_controller, existing_loan, accounts +): user = accounts[0] someone_else = accounts[1] c_amount = int(2 * 1e6 * 1e18 * 1.5 / 3000) @@ -60,7 +70,7 @@ def test_repay_all(stablecoin, collateral_token, market_controller, existing_loa # because health is still good and loan is not underwater with boa.env.prank(someone_else): - stablecoin.approve(market_controller, 2**256-1) + stablecoin.approve(market_controller, 2**256 - 1) market_controller.repay(2**100, user) assert market_controller.debt(user) == 0 assert stablecoin.balanceOf(user) == 0 @@ -70,7 +80,9 @@ def test_repay_all(stablecoin, collateral_token, market_controller, existing_loa assert market_controller.total_debt() == 0 -def test_borrow_more(stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts): +def test_borrow_more( + stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts +): user = accounts[0] someone_else = accounts[1] @@ -103,7 +115,9 @@ def test_borrow_more(stablecoin, collateral_token, market_controller, existing_l assert market_controller.total_debt() == debt + more_debt -def test_remove_collateral(stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts): +def test_remove_collateral( + stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts +): user = accounts[0] someone_else = accounts[1] @@ -129,8 +143,15 @@ def test_remove_collateral(stablecoin, collateral_token, market_controller, exis @pytest.fixture(scope="module") -def controller_for_liquidation(stablecoin, collateral_token, market_controller, market_amm, - price_oracle, monetary_policy, admin): +def controller_for_liquidation( + stablecoin, + collateral_token, + market_controller, + market_amm, + price_oracle, + monetary_policy, + admin, +): def f(sleep_time, user, someone_else): N = 5 collateral_amount = 10**18 @@ -141,10 +162,10 @@ def f(sleep_time, user, someone_else): debt = market_controller.max_borrowable(collateral_amount, N) with boa.env.prank(user): - collateral_token._mint_for_testing(user, collateral_amount) - stablecoin.approve(market_amm, 2**256-1) - stablecoin.approve(market_controller, 2**256-1) - collateral_token.approve(market_controller, 2**256-1) + boa.deal(collateral_token, user, collateral_amount) + stablecoin.approve(market_amm, 2**256 - 1) + stablecoin.approve(market_controller, 2**256 - 1) + collateral_token.approve(market_controller, 2**256 - 1) market_controller.create_loan(collateral_amount, debt, N) health_0 = market_controller.health(user) @@ -167,22 +188,21 @@ def f(sleep_time, user, someone_else): monetary_policy.set_rate(0) # Ensure approved account has enough to liquidate - with boa.env.prank(someone_else): - collateral_token._mint_for_testing(someone_else, collateral_amount) - stablecoin.approve(market_amm, 2**256-1) - stablecoin.approve(market_controller, 2**256-1) - collateral_token.approve(market_controller, 2**256-1) - market_controller.create_loan(collateral_amount, debt, N) + boa.deal(stablecoin, someone_else, debt) return market_controller return f -def test_self_liquidate(stablecoin, collateral_token, controller_for_liquidation, market_amm, accounts): +def test_self_liquidate( + stablecoin, collateral_token, controller_for_liquidation, market_amm, accounts +): user = accounts[1] someone_else = accounts[2] - controller = controller_for_liquidation(sleep_time=40 * 86400, user=user, someone_else=someone_else) + controller = controller_for_liquidation( + sleep_time=30 * 86400, user=user, someone_else=someone_else + ) x = market_amm.get_sum_xy(user)[0] diff --git a/tests/stableborrow/test_bigfuzz.py b/tests/stableborrow/test_bigfuzz.py index c0e14442..4eae21f8 100644 --- a/tests/stableborrow/test_bigfuzz.py +++ b/tests/stableborrow/test_bigfuzz.py @@ -4,8 +4,14 @@ from boa import BoaError from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) +from tests.utils.deployers import AMM_DEPLOYER, LEND_CONTROLLER_DEPLOYER # Variables and methods to check # * A @@ -16,8 +22,6 @@ # * set_debt_ceiling # * set_borrowing_discounts # * collect AMM fees - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" USE_FRACTION = 1 USE_CALLBACKS = 2 @@ -36,7 +40,9 @@ class BigFuzz(RuleBasedStateMachine): liquidator_id = st.integers(min_value=0, max_value=9) time_shift = st.integers(min_value=1, max_value=30 * 86400) - debt_ceiling_change = st.integers(min_value=-10**6 * 10**18, max_value=10**6 * 10**18) + debt_ceiling_change = st.integers( + min_value=-(10**6) * 10**18, max_value=10**6 * 10**18 + ) extended_mode = st.integers(min_value=0, max_value=2) liquidate_frac = st.integers(min_value=0, max_value=10**18 + 1) @@ -44,9 +50,11 @@ class BigFuzz(RuleBasedStateMachine): def __init__(self): super().__init__() self.A = self.market_amm.A() - self.debt_ceiling = self.controller_factory.debt_ceiling(self.market_controller.address) + self.debt_ceiling = self.controller_factory.debt_ceiling( + self.market_controller.address + ) self.fees = 0 - self.collateral_mul = 10**(18 - self.collateral_token.decimals()) + self.collateral_mul = 10 ** (18 - self.collateral_token.decimals()) # Auxiliary methods # def collect_fees(self): @@ -55,8 +63,12 @@ def collect_fees(self): self.market_controller.collect_fees() except BoaError: with boa.env.prank(self.admin): - self.controller_factory.collect_fees_above_ceiling(self.market_controller.address) - self.debt_ceiling = self.controller_factory.debt_ceiling(self.market_controller.address) + self.controller_factory.collect_fees_above_ceiling( + self.market_controller.address + ) + self.debt_ceiling = self.controller_factory.debt_ceiling( + self.market_controller.address + ) fees = self.stablecoin.balanceOf(self.accounts[0]) - fees self.fees += fees @@ -79,7 +91,11 @@ def check_debt_ceiling(self, amount): return self.market_controller.total_debt() + amount <= self.debt_ceiling def get_max_good_band(self): - return ceil(log2(self.market_amm.get_base_price() / self.market_amm.price_oracle()) / log2(self.A / (self.A - 1)) + 5) + return ceil( + log2(self.market_amm.get_base_price() / self.market_amm.price_oracle()) + / log2(self.A / (self.A - 1)) + + 5 + ) # Borrowing and returning # @rule(y=collateral_amount, n=n, uid=user_id, ratio=ratio) @@ -88,15 +104,19 @@ def deposit(self, y, n, ratio, uid): y = y // self.collateral_mul user = self.accounts[uid] with boa.env.prank(user): - self.collateral_token._mint_for_testing(user, y) + boa.deal(self.collateral_token, user, y) max_debt = self.market_controller.max_borrowable(y, n) if not self.check_debt_ceiling(debt): with boa.reverts(): self.market_controller.create_loan(y, debt, n) return - if (debt > max_debt or y * self.collateral_mul // n <= 100 or debt == 0 - or self.market_controller.loan_exists(user)): - if debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40)): + if ( + debt > max_debt + or y * self.collateral_mul // n <= 100 + or debt == 0 + or self.market_controller.loan_exists(user) + ): + if debt < max_debt / (0.9999 - 20 / (y * self.collateral_mul + 40)): try: self.market_controller.create_loan(y, debt, n) except Exception: @@ -106,7 +126,7 @@ def deposit(self, y, n, ratio, uid): self.market_controller.create_loan(y, debt, n) except Exception: return - assert debt < max_debt * (self.A / (self.A - 1))**0.4 + assert debt < max_debt * (self.A / (self.A - 1)) ** 0.4 return else: try: @@ -133,13 +153,19 @@ def repay(self, ratio, uid): with boa.reverts(): self.market_controller.repay(amount, user) else: - if amount > 0 and ( - (amount >= debt and (debt > self.stablecoin.balanceOf(user) + self.market_amm.get_sum_xy(user)[0])) - or (amount < debt and (amount > self.stablecoin.balanceOf(user)))): - with boa.reverts(): + if amount > 0: + if ( + amount >= debt + and ( + debt + > self.stablecoin.balanceOf(user) + + self.market_amm.get_sum_xy(user)[0] + ) + ) or (amount < debt and (amount > self.stablecoin.balanceOf(user))): + with boa.reverts(): + self.market_controller.repay(amount, user) + else: self.market_controller.repay(amount, user) - else: - self.market_controller.repay(amount, user) self.remove_stablecoins(user) @rule(y=collateral_amount, uid=user_id) @@ -150,10 +176,14 @@ def add_collateral(self, y, uid): if exists: n1, n2 = self.market_amm.read_user_tick_numbers(user) n0 = self.market_amm.active_band() - self.collateral_token._mint_for_testing(user, y) + boa.deal(self.collateral_token, user, y) with boa.env.prank(user): - if (exists and n1 > n0 and self.market_amm.p_oracle_up(n1) < self.market_amm.price_oracle()) or y == 0: + if ( + exists + and n1 > n0 + and self.market_amm.p_oracle_up(n1) < self.market_amm.price_oracle() + ) or y == 0: self.market_controller.add_collateral(y, user) else: with boa.reverts(): @@ -163,7 +193,9 @@ def add_collateral(self, y, uid): def remove_collateral(self, y, uid): y = y // self.collateral_mul user = self.accounts[uid] - user_collateral, user_stablecoin, debt, N = self.market_controller.user_state(user) + user_collateral, user_stablecoin, debt, N = self.market_controller.user_state( + user + ) if debt > 0: n1, n2 = self.market_amm.read_user_tick_numbers(user) n0 = self.market_amm.active_band() @@ -196,7 +228,7 @@ def remove_collateral(self, y, uid): def borrow_more(self, y, ratio, uid): y = y // self.collateral_mul user = self.accounts[uid] - self.collateral_token._mint_for_testing(user, y) + boa.deal(self.collateral_token, user, y) with boa.env.prank(user): if not self.market_controller.loan_exists(user): @@ -207,7 +239,13 @@ def borrow_more(self, y, ratio, uid): sx, sy = self.market_amm.get_sum_xy(user) n1, n2 = self.market_amm.read_user_tick_numbers(user) n = n2 - n1 + 1 - amount = int(self.market_amm.price_oracle() * (sy + y) * self.collateral_mul / 1e18 * ratio) + amount = int( + self.market_amm.price_oracle() + * (sy + y) + * self.collateral_mul + / 1e18 + * ratio + ) current_debt = self.market_controller.debt(user) final_debt = current_debt + amount @@ -217,9 +255,13 @@ def borrow_more(self, y, ratio, uid): return if sx == 0 or amount == 0: - max_debt = self.market_controller.max_borrowable(sy + y, n, current_debt) + max_debt = self.market_controller.max_borrowable( + sy + y, n, current_debt + ) if final_debt > max_debt and amount > 0: - if final_debt < max_debt / (0.9999 - 20/(y * self.collateral_mul + 40) - 1e-9): + if final_debt < max_debt / ( + 0.9999 - 20 / (y * self.collateral_mul + 40) - 1e-9 + ): try: self.market_controller.borrow_more(y, amount) except Exception: @@ -231,7 +273,10 @@ def borrow_more(self, y, ratio, uid): try: self.market_controller.borrow_more(y, amount) except Exception: - if self.get_max_good_band() > self.market_amm.active_band_with_skip(): + if ( + self.get_max_good_band() + > self.market_amm.active_band_with_skip() + ): # Otherwise (if price desync is too large) - this fail is to be expected raise @@ -249,7 +294,7 @@ def trade_to_price(self, p): if is_pump: self.market_amm.exchange(0, 1, amount, 0) else: - self.collateral_token._mint_for_testing(user, amount) + boa.deal(self.collateral_token, user, amount) self.market_amm.exchange(1, 0, amount, 0) @rule(r=ratio, is_pump=is_pump, uid=user_id) @@ -272,7 +317,7 @@ def trade(self, r, is_pump, uid): pass else: amount = int(r * self.collateral_token.totalSupply()) - self.collateral_token._mint_for_testing(user, amount) + boa.deal(self.collateral_token, user, amount) self.market_amm.exchange(1, 0, amount, 0) self.remove_stablecoins(user) @@ -290,18 +335,19 @@ def self_liquidate_and_health(self, emode, frac): with boa.env.prank(user): if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate(user, 0, frac) except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return raise elif emode == USE_CALLBACKS: - self.stablecoin.transfer(self.fake_leverage.address, self.stablecoin.balanceOf(user)) + self.stablecoin.transfer( + self.fake_leverage.address, self.stablecoin.balanceOf(user) + ) try: - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.market_controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -329,55 +375,65 @@ def liquidate(self, uid, luid, emode, frac): with boa.env.prank(liquidator): with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate(user, 0, frac) elif emode == USE_CALLBACKS: - self.stablecoin.transfer(self.fake_leverage.address, self.stablecoin.balanceOf(user)) - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.stablecoin.transfer( + self.fake_leverage.address, self.stablecoin.balanceOf(user) + ) + self.market_controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: - self.stablecoin.transferFrom(self.fake_leverage.address, liquidator, - self.stablecoin.balanceOf(self.fake_leverage.address)) + self.stablecoin.transferFrom( + self.fake_leverage.address, + liquidator, + self.stablecoin.balanceOf(self.fake_leverage.address), + ) else: health_limit = self.market_controller.liquidation_discount() try: health = self.market_controller.health(user, True) except Exception as e: - assert 'Too deep' in str(e) + assert "Too deep" in str(e) with boa.env.prank(liquidator): if health >= health_limit: with boa.reverts(): if emode == USE_FRACTION: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate(user, 0, frac) elif emode == USE_CALLBACKS: - self.stablecoin.transfer(self.fake_leverage.address, self.stablecoin.balanceOf(user)) - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.stablecoin.transfer( + self.fake_leverage.address, + self.stablecoin.balanceOf(user), + ) + self.market_controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) else: self.market_controller.liquidate(user, 0) if emode == USE_CALLBACKS: - self.stablecoin.transferFrom(self.fake_leverage.address, liquidator, - self.stablecoin.balanceOf(self.fake_leverage.address)) + self.stablecoin.transferFrom( + self.fake_leverage.address, + liquidator, + self.stablecoin.balanceOf(self.fake_leverage.address), + ) else: if emode == USE_FRACTION: try: - self.market_controller.liquidate_extended( - user, 0, frac, ZERO_ADDRESS, []) + self.market_controller.liquidate(user, 0, frac) except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return raise elif emode == USE_CALLBACKS: - self.stablecoin.transfer(self.fake_leverage.address, self.stablecoin.balanceOf(user)) + self.stablecoin.transfer( + self.fake_leverage.address, self.stablecoin.balanceOf(user) + ) try: - self.market_controller.liquidate_extended( - user, 0, frac, - self.fake_leverage.address, []) + self.market_controller.liquidate( + user, 0, frac, self.fake_leverage.address, b"" + ) except Exception: if self.market_controller.debt(user) * frac // 10**18 == 0: return @@ -416,21 +472,33 @@ def debt_supply(self): total_debt = self.market_controller.total_debt() if total_debt == 0: assert self.market_controller.minted() == self.market_controller.redeemed() - assert total_debt == self.stablecoin.totalSupply() - self.stablecoin.balanceOf(self.market_controller.address) - assert abs(sum(self.market_controller.debt(u) for u in self.accounts) - total_debt) <= 10 + assert total_debt == self.stablecoin.totalSupply() - self.stablecoin.balanceOf( + self.market_controller.address + ) + assert ( + abs(sum(self.market_controller.debt(u) for u in self.accounts) - total_debt) + <= 10 + ) # 10 accounts = 10 wei error? @invariant() def minted_redeemed(self): - assert self.market_controller.redeemed() + self.market_controller.total_debt() >= self.market_controller.minted() + assert ( + self.market_controller.redeemed() + self.market_controller.total_debt() + >= self.market_controller.minted() + ) # Debt ceiling @rule(d_ceil=debt_ceiling_change) def change_debt_ceiling(self, d_ceil): - current_ceil = self.controller_factory.debt_ceiling(self.market_controller.address) + current_ceil = self.controller_factory.debt_ceiling( + self.market_controller.address + ) new_ceil = max(current_ceil + d_ceil, 0) with boa.env.prank(self.admin): - self.controller_factory.set_debt_ceiling(self.market_controller.address, new_ceil) + self.controller_factory.set_debt_ceiling( + self.market_controller.address, new_ceil + ) self.debt_ceiling = new_ceil @rule() @@ -442,19 +510,36 @@ def rug_debt_ceiling(self): redeemed = self.market_controller.redeemed() if total_debt == 0 and redeemed + total_debt == minted: # Debt is 0 and admin fees are claimed - ceiling = self.controller_factory.debt_ceiling(self.market_controller.address) + ceiling = self.controller_factory.debt_ceiling( + self.market_controller.address + ) assert self.stablecoin.balanceOf(self.market_controller.address) == ceiling -@pytest.mark.parametrize("_tmp", range(4)) # This splits the test into 8 small chunks which are easier to parallelize +@pytest.mark.parametrize( + "_tmp", range(4) +) # This splits the test into 8 small chunks which are easier to parallelize @pytest.mark.parametrize("collateral_digits", [8, 18]) def test_big_fuzz( - controller_factory, get_market, monetary_policy, get_collateral_token, collateral_digits, stablecoin, price_oracle, - accounts, get_fake_leverage, admin, amm_interface, controller_interface, _tmp): - collateral_token = get_collateral_token(collateral_digits) + controller_factory, + get_market, + monetary_policy, + collateral_digits, + stablecoin, + price_oracle, + accounts, + get_fake_leverage, + admin, + _tmp, +): + from tests.utils.deployers import ERC20_MOCK_DEPLOYER + + collateral_token = ERC20_MOCK_DEPLOYER.deploy(collateral_digits) market = get_market(collateral_token) - market_amm = amm_interface.at(market.get_amm(collateral_token.address)) - market_controller = controller_interface.at(market.get_controller(collateral_token.address)) + market_amm = AMM_DEPLOYER.at(market.get_amm(collateral_token.address)) + market_controller = LEND_CONTROLLER_DEPLOYER.at( + market.get_controller(collateral_token.address) + ) fake_leverage = get_fake_leverage(collateral_token, market_controller) BigFuzz.TestCase.settings = settings(max_examples=50, stateful_step_count=20) @@ -466,7 +551,17 @@ def test_big_fuzz( def test_noraise( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -480,7 +575,17 @@ def test_noraise( def test_noraise_2( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): # This is due to evmdiv working not like floor div (fixed) for k, v in locals().items(): setattr(BigFuzz, k, v) @@ -491,7 +596,17 @@ def test_noraise_2( def test_exchange_fails( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): # This is due to evmdiv working not like floor div (fixed) for k, v in locals().items(): setattr(BigFuzz, k, v) @@ -512,7 +627,17 @@ def test_exchange_fails( def test_noraise_3( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -522,7 +647,17 @@ def test_noraise_3( def test_repay_error_1( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -533,7 +668,17 @@ def test_repay_error_1( def test_not_enough_collateral( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -548,7 +693,17 @@ def test_not_enough_collateral( def test_noraise_4( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -559,7 +714,17 @@ def test_noraise_4( def test_debt_nonequal( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -571,7 +736,17 @@ def test_debt_nonequal( def test_noraise_5( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -587,7 +762,17 @@ def test_noraise_5( def test_add_collateral_fail( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -611,7 +796,17 @@ def test_add_collateral_fail( def test_debt_eq_repay_no_coins( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -631,7 +826,17 @@ def test_debt_eq_repay_no_coins( def test_amount_not_too_low( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -643,7 +848,17 @@ def test_amount_not_too_low( def test_debt_too_high( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -655,7 +870,17 @@ def test_debt_too_high( def test_debt_too_high_2( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) with boa.env.anchor(): @@ -666,7 +891,17 @@ def test_debt_too_high_2( def test_change_debt_ceiling_error( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -678,7 +913,17 @@ def test_change_debt_ceiling_error( def test_borrow_zero_norevert( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -689,7 +934,17 @@ def test_borrow_zero_norevert( def test_debt_too_high_3( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -700,7 +955,17 @@ def test_debt_too_high_3( def test_debt_too_high_4( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -711,7 +976,17 @@ def test_debt_too_high_4( def test_loan_doesnt_exist( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -736,7 +1011,17 @@ def test_loan_doesnt_exist( def test_debt_too_high_2_users( - controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, price_oracle, accounts, fake_leverage, admin): + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + price_oracle, + accounts, + fake_leverage, + admin, +): for k, v in locals().items(): setattr(BigFuzz, k, v) state = BigFuzz() @@ -759,8 +1044,18 @@ def test_debt_too_high_2_users( def test_cannot_create_loan( - controller_factory, get_market, amm_interface, controller_interface, monetary_policy, stablecoin, price_oracle, - accounts, get_fake_leverage, admin, collateral_token, market_amm, market_controller): + controller_factory, + get_market, + monetary_policy, + stablecoin, + price_oracle, + accounts, + get_fake_leverage, + admin, + collateral_token, + market_amm, + market_controller, +): fake_leverage = get_fake_leverage(collateral_token, market_controller) for k, v in locals().items(): setattr(BigFuzz, k, v) diff --git a/tests/stableborrow/test_calculate_n1.py b/tests/stableborrow/test_calculate_n1.py index c7303b3d..b62b74b5 100644 --- a/tests/stableborrow/test_calculate_n1.py +++ b/tests/stableborrow/test_calculate_n1.py @@ -12,19 +12,21 @@ def test_n1(market_amm, market_controller, collateral, debt, n): n0 = market_amm.active_band() A = market_amm.A() p0 = market_amm.p_oracle_down(n0) / 1e18 - discounted_collateral = collateral * (10**18 - market_controller.loan_discount()) // 10**18 + discounted_collateral = ( + collateral * (10**18 - market_controller.loan_discount()) // 10**18 + ) too_high = False too_deep = False try: n1 = market_controller.calculate_debt_n1(collateral, debt, n) except Exception as e: - too_high = 'Debt too high' in str(e) - too_deep = 'Too deep' in str(e) + too_high = "Debt too high" in str(e) + too_deep = "Too deep" in str(e) if not too_high and not too_deep: raise if too_high: - assert discounted_collateral * p0 * ((A - 1) / A)**n <= debt + assert discounted_collateral * p0 * ((A - 1) / A) ** n <= debt return if too_deep: assert abs(log2(debt / discounted_collateral * p0)) > log2(500) diff --git a/tests/stableborrow/test_create_repay.py b/tests/stableborrow/test_create_repay.py index e2cfe31e..c201836d 100644 --- a/tests/stableborrow/test_create_repay.py +++ b/tests/stableborrow/test_create_repay.py @@ -2,15 +2,22 @@ import pytest from hypothesis import given from hypothesis import strategies as st -from ..conftest import approx -def test_create_loan(controller_factory, stablecoin, collateral_token, market_controller, market_amm, monetary_policy, accounts): +def test_create_loan( + controller_factory, + stablecoin, + collateral_token, + market_controller, + market_amm, + monetary_policy, + accounts, +): user = accounts[0] with boa.env.anchor(): with boa.env.prank(user): initial_amount = 10**25 - collateral_token._mint_for_testing(user, initial_amount) + boa.deal(collateral_token, user, initial_amount) c_amount = int(2 * 1e6 * 1e18 * 1.5 / 3000) l_amount = 2 * 10**6 * 10**18 @@ -18,9 +25,9 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co market_controller.create_loan(c_amount, l_amount, 5) l_amount = 5 * 10**5 * 10**18 - with boa.reverts('Need more ticks'): + with boa.reverts("Need more ticks"): market_controller.create_loan(c_amount, l_amount, 3) - with boa.reverts('Need less ticks'): + with boa.reverts("Need less ticks"): market_controller.create_loan(c_amount, l_amount, 400) with boa.reverts("Debt too high"): @@ -29,25 +36,29 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co # Phew, the loan finally was created market_controller.create_loan(c_amount, l_amount, 5) # But cannot do it again - with boa.reverts('Loan already created'): + with boa.reverts("Loan already created"): market_controller.create_loan(c_amount, 1, 5) assert stablecoin.balanceOf(user) == l_amount - assert l_amount == stablecoin.totalSupply() - stablecoin.balanceOf(market_controller) + assert l_amount == stablecoin.totalSupply() - stablecoin.balanceOf( + market_controller + ) assert collateral_token.balanceOf(user) == initial_amount - c_amount assert market_controller.total_debt() == l_amount assert market_controller.debt(user) == l_amount p_up, p_down = market_controller.user_prices(user) - p_lim = l_amount / c_amount / (1 - market_controller.loan_discount()/1e18) - assert approx(p_lim, (p_down * p_up)**0.5 / 1e18, 2 / market_amm.A()) + p_lim = l_amount / c_amount / (1 - market_controller.loan_discount() / 1e18) + assert p_lim == pytest.approx( + (p_down * p_up) ** 0.5 / 1e18, rel=2 / market_amm.A() + ) h = market_controller.health(user) / 1e18 + 0.02 assert h >= 0.05 and h <= 0.06 h = market_controller.health(user, True) / 1e18 + 0.02 - assert approx(h, c_amount * 3000 / l_amount - 1, 0.02) + assert h == pytest.approx(c_amount * 3000 / l_amount - 1, rel=0.02) @given( @@ -56,8 +67,10 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co ) def test_max_borrowable(market_controller, accounts, collateral_amount, n): max_borrowable = market_controller.max_borrowable(collateral_amount, n) - with boa.reverts('Debt too high'): - market_controller.calculate_debt_n1(collateral_amount, int(max_borrowable * 1.001), n) + with boa.reverts("Debt too high"): + market_controller.calculate_debt_n1( + collateral_amount, int(max_borrowable * 1.001), n + ) market_controller.calculate_debt_n1(collateral_amount, max_borrowable, n) @@ -69,17 +82,19 @@ def existing_loan(collateral_token, market_controller, accounts): n = 5 with boa.env.prank(user): - collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) market_controller.create_loan(c_amount, l_amount, n) -def test_repay_all(stablecoin, collateral_token, market_controller, existing_loan, accounts): +def test_repay_all( + stablecoin, collateral_token, market_controller, existing_loan, accounts +): user = accounts[0] with boa.env.anchor(): with boa.env.prank(user): c_amount = int(2 * 1e6 * 1e18 * 1.5 / 3000) amm = market_controller.amm() - stablecoin.approve(market_controller, 2**256-1) + stablecoin.approve(market_controller, 2**256 - 1) market_controller.repay(2**100, user) assert market_controller.debt(user) == 0 assert stablecoin.balanceOf(user) == 0 @@ -89,7 +104,9 @@ def test_repay_all(stablecoin, collateral_token, market_controller, existing_loa assert market_controller.total_debt() == 0 -def test_repay_half(stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts): +def test_repay_half( + stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts +): user = accounts[0] with boa.env.anchor(): @@ -99,7 +116,7 @@ def test_repay_half(stablecoin, collateral_token, market_controller, existing_lo to_repay = debt // 2 n_before_0, n_before_1 = market_amm.read_user_tick_numbers(user) - stablecoin.approve(market_controller, 2**256-1) + stablecoin.approve(market_controller, 2**256 - 1) market_controller.repay(to_repay, user) n_after_0, n_after_1 = market_amm.read_user_tick_numbers(user) @@ -115,7 +132,9 @@ def test_repay_half(stablecoin, collateral_token, market_controller, existing_lo assert market_controller.total_debt() == debt - to_repay -def test_add_collateral(stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts): +def test_add_collateral( + stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts +): user = accounts[0] with boa.env.anchor(): @@ -124,7 +143,7 @@ def test_add_collateral(stablecoin, collateral_token, market_controller, existin n_before_0, n_before_1 = market_amm.read_user_tick_numbers(user) with boa.env.prank(user): - collateral_token._mint_for_testing(user, c_amount) + boa.deal(collateral_token, user, c_amount) market_controller.add_collateral(c_amount, user) n_after_0, n_after_1 = market_amm.read_user_tick_numbers(user) @@ -140,7 +159,9 @@ def test_add_collateral(stablecoin, collateral_token, market_controller, existin assert market_controller.total_debt() == debt -def test_borrow_more(stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts): +def test_borrow_more( + stablecoin, collateral_token, market_controller, existing_loan, market_amm, accounts +): user = accounts[0] with boa.env.anchor(): diff --git a/tests/stableborrow/test_create_repay_stateful.py b/tests/stableborrow/test_create_repay_stateful.py index 358090fb..5390be00 100644 --- a/tests/stableborrow/test_create_repay_stateful.py +++ b/tests/stableborrow/test_create_repay_stateful.py @@ -1,19 +1,25 @@ """ Stateful test to create and repay loans without moving the price oracle """ + import boa from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class StatefulLendBorrow(RuleBasedStateMachine): n = st.integers(min_value=5, max_value=50) - amount = st.integers(min_value=0, max_value=2**256-1) - c_amount = st.integers(min_value=0, max_value=2**256-1) + amount = st.integers(min_value=0, max_value=2**256 - 1) + c_amount = st.integers(min_value=0, max_value=2**256 - 1) user_id = st.integers(min_value=0, max_value=9) def __init__(self): @@ -23,8 +29,8 @@ def __init__(self): self.debt_ceiling = self.controller_factory.debt_ceiling(self.controller) for u in self.accounts: with boa.env.prank(u): - self.collateral_token.approve(self.controller, 2**256-1) - self.stablecoin.approve(self.controller, 2**256-1) + self.collateral_token.approve(self.controller, 2**256 - 1) + self.stablecoin.approve(self.controller, 2**256 - 1) @rule(c_amount=c_amount, amount=amount, n=n, user_id=user_id) def create_loan(self, c_amount, amount, n, user_id): @@ -32,7 +38,7 @@ def create_loan(self, c_amount, amount, n, user_id): with boa.env.prank(user): if self.controller.loan_exists(user): - with boa.reverts('Loan already created'): + with boa.reverts("Loan already created"): self.controller.create_loan(c_amount, amount, n) return @@ -40,16 +46,17 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.calculate_debt_n1(c_amount, amount, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.create_loan(c_amount, amount, n) return if self.controller.total_debt() + amount > self.debt_ceiling: if ( - (self.controller.total_debt() + amount) * self.amm.get_rate_mul() > 2**256 - 1 - or c_amount * self.amm.get_p() > 2**256 - 1 + (self.controller.total_debt() + amount) * self.amm.get_rate_mul() + > 2** 256 - 1 + or c_amount * self.amm.get_p() > 2** 256 - 1 ): with boa.reverts(): self.controller.create_loan(c_amount, amount, n) @@ -59,13 +66,13 @@ def create_loan(self, c_amount, amount, n, user_id): return if amount == 0: - with boa.reverts('No loan'): + with boa.reverts("No loan"): self.controller.create_loan(c_amount, amount, n) # It's actually division by zero which happens return try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(self.collateral_token, user, c_amount) except Exception: return # Probably overflow @@ -83,8 +90,11 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.create_loan(c_amount, amount, n) except Exception as e: - if c_amount // n > 2 * DEAD_SHARES and c_amount // n < (2**128 - 1) // DEAD_SHARES: - if 'Too deep' not in str(e): + if ( + c_amount // n > 2 * DEAD_SHARES + and c_amount // n < (2**128 - 1) // DEAD_SHARES + ): + if "Too deep" not in str(e): raise @rule(amount=amount, user_id=user_id) @@ -92,7 +102,6 @@ def repay(self, amount, user_id): user = self.accounts[user_id] with boa.env.prank(user): if amount == 0: - self.controller.repay(amount, user) return if not self.controller.loan_exists(user): with boa.reverts("Loan doesn't exist"): @@ -110,7 +119,7 @@ def add_collateral(self, c_amount, user_id): return try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(self.collateral_token, user, c_amount) except Exception: return # Probably overflow @@ -119,7 +128,9 @@ def add_collateral(self, c_amount, user_id): self.controller.add_collateral(c_amount, user) return - if (c_amount + self.amm.get_sum_xy(user)[1]) * self.amm.get_p() > 2**256 - 1: + if ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.amm.get_p() > 2**256 - 1: with boa.reverts(): self.controller.add_collateral(c_amount, user) return @@ -136,7 +147,7 @@ def borrow_more(self, c_amount, amount, user_id): with boa.env.prank(user): try: - self.collateral_token._mint_for_testing(user, c_amount) + boa.deal(self.collateral_token, user, c_amount) except Exception: return # Probably overflow @@ -160,14 +171,16 @@ def borrow_more(self, c_amount, amount, user_id): try: self.controller.calculate_debt_n1(final_collateral, final_debt, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.borrow_more(c_amount, amount) return if self.controller.total_debt() + amount > self.debt_ceiling: - if (self.controller.total_debt() + amount) * self.amm.get_rate_mul() > 2**256 - 1: + if ( + self.controller.total_debt() + amount + ) * self.amm.get_rate_mul() > 2**256 - 1: with boa.reverts(): self.controller.borrow_more(c_amount, amount) else: @@ -188,11 +201,18 @@ def borrow_more(self, c_amount, amount, user_id): @invariant() def debt_supply(self): - assert self.controller.total_debt() == self.stablecoin.totalSupply() - self.stablecoin.balanceOf(self.controller) + assert ( + self.controller.total_debt() + == self.stablecoin.totalSupply() + - self.stablecoin.balanceOf(self.controller) + ) @invariant() def sum_of_debts(self): - assert sum(self.controller.debt(u) for u in self.accounts) == self.controller.total_debt() + assert ( + sum(self.controller.debt(u) for u in self.accounts) + == self.controller.total_debt() + ) @invariant() def health(self): @@ -201,14 +221,30 @@ def health(self): assert self.controller.health(user) > 0 -def test_stateful_lendborrow(controller_factory, market_amm, market_controller, collateral_token, stablecoin, accounts): - StatefulLendBorrow.TestCase.settings = settings(max_examples=500, stateful_step_count=20) +def test_stateful_lendborrow( + controller_factory, + market_amm, + market_controller, + collateral_token, + stablecoin, + accounts, +): + StatefulLendBorrow.TestCase.settings = settings( + max_examples=500, stateful_step_count=20 + ) for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) run_state_machine_as_test(StatefulLendBorrow) -def test_bad_health_underflow(controller_factory, market_amm, market_controller, collateral_token, stablecoin, accounts): +def test_bad_health_underflow( + controller_factory, + market_amm, + market_controller, + collateral_token, + stablecoin, + accounts, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): @@ -217,26 +253,55 @@ def test_bad_health_underflow(controller_factory, market_amm, market_controller, state.health() -def test_overflow(controller_factory, market_amm, market_controller, collateral_token, stablecoin, accounts): +def test_overflow( + controller_factory, + market_amm, + market_controller, + collateral_token, + stablecoin, + accounts, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): state = StatefulLendBorrow() state.create_loan( amount=407364794483206832621538773467837164307398905518629081113581615337081836, - c_amount=41658360764272065869638360137931952069431923873907374062, n=5, user_id=0) - - -def test_health_overflow(controller_factory, market_amm, market_controller, collateral_token, stablecoin, accounts): + c_amount=41658360764272065869638360137931952069431923873907374062, + n=5, + user_id=0, + ) + + +def test_health_overflow( + controller_factory, + market_amm, + market_controller, + collateral_token, + stablecoin, + accounts, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): state = StatefulLendBorrow() - state.create_loan(amount=256, c_amount=2787635851270792912435800128182537894764544, n=5, user_id=0) + state.create_loan( + amount=256, + c_amount=2787635851270792912435800128182537894764544, + n=5, + user_id=0, + ) state.health() -def test_health_underflow_2(controller_factory, market_amm, market_controller, collateral_token, stablecoin, accounts): +def test_health_underflow_2( + controller_factory, + market_amm, + market_controller, + collateral_token, + stablecoin, + accounts, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): diff --git a/tests/stableborrow/test_extra_health.py b/tests/stableborrow/test_extra_health.py index 3d501e1b..9c1d4b55 100644 --- a/tests/stableborrow/test_extra_health.py +++ b/tests/stableborrow/test_extra_health.py @@ -6,13 +6,21 @@ @given( extra_health=st.integers(min_value=0, max_value=2 * 10**18), ) -def test_create_loan(controller_factory, stablecoin, collateral_token, market_controller, market_amm, monetary_policy, - accounts, extra_health): +def test_create_loan( + controller_factory, + stablecoin, + collateral_token, + market_controller, + market_amm, + monetary_policy, + accounts, + extra_health, +): user = accounts[0] with boa.env.prank(user): initial_amount = 10**25 - collateral_token._mint_for_testing(user, initial_amount) + boa.deal(collateral_token, user, initial_amount) c_amount = int(2 * 1e6 * 1e18 * 1.5 / 3000) max_l_amount = market_controller.max_borrowable(c_amount, 5) loan_discount = market_controller.loan_discount() / 1e18 @@ -23,7 +31,15 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co except Exception: assert extra_health > 0 - l_amount = max(int(max_l_amount * (1 - extra_health / 1e18 - loan_discount) / (1 - loan_discount) / 2), 0) + l_amount = max( + int( + max_l_amount + * (1 - extra_health / 1e18 - loan_discount) + / (1 - loan_discount) + / 2 + ), + 0, + ) if l_amount > 1000: market_controller.create_loan(c_amount, l_amount, 5) @@ -32,17 +48,34 @@ def test_create_loan(controller_factory, stablecoin, collateral_token, market_co @given( collateral_amount=st.integers(min_value=10**9, max_value=10**20), n=st.integers(min_value=5, max_value=50), - extra_health=st.integers(min_value=0, max_value=9 * 10**17) + extra_health=st.integers(min_value=0, max_value=9 * 10**17), ) -def test_max_borrowable(market_controller, accounts, collateral_amount, n, extra_health): +def test_max_borrowable( + market_controller, accounts, collateral_amount, n, extra_health +): max_borrowable = market_controller.max_borrowable(collateral_amount, n) market_controller.calculate_debt_n1(collateral_amount, max_borrowable, n) loan_discount = market_controller.loan_discount() / 1e18 with boa.env.prank(accounts[0]): market_controller.set_extra_health(extra_health) - max_borrowable_user = market_controller.max_borrowable(collateral_amount, n, 0, accounts[0]) + max_borrowable_user = market_controller.max_borrowable( + collateral_amount, n, 0, accounts[0] + ) if extra_health > 10**16: assert max_borrowable_user < max_borrowable - assert abs(max_borrowable_user - max(max_borrowable * (1 - extra_health / 1e18 - loan_discount) / (1 - loan_discount), 0)) < 1e-4 * 1e18 - market_controller.calculate_debt_n1(collateral_amount, max_borrowable_user, n, accounts[0]) + assert ( + abs( + max_borrowable_user + - max( + max_borrowable + * (1 - extra_health / 1e18 - loan_discount) + / (1 - loan_discount), + 0, + ) + ) + < 1e-4 * 1e18 + ) + market_controller.calculate_debt_n1( + collateral_amount, max_borrowable_user, n, accounts[0] + ) diff --git a/tests/stableborrow/test_factory.py b/tests/stableborrow/test_factory.py index 6e6d1d00..d3a51cd6 100644 --- a/tests/stableborrow/test_factory.py +++ b/tests/stableborrow/test_factory.py @@ -1,4 +1,5 @@ import boa +from tests.utils.deployers import AMM_DEPLOYER, LEND_CONTROLLER_DEPLOYER def test_stablecoin_admin(controller_factory, stablecoin, accounts): @@ -20,8 +21,9 @@ def test_impl(controller_factory, controller_impl, amm_impl): assert controller_factory.amm_implementation() == amm_impl.address -def test_add_market(controller_factory, collateral_token, price_oracle, monetary_policy, admin, - controller_interface, amm_interface): +def test_add_market( + controller_factory, collateral_token, price_oracle, monetary_policy, admin +): # token: address, A: uint256, fee: uint256, admin_fee: uint256, # _price_oracle_contract: address, # monetary_policy: address, loan_discount: uint256, liquidation_discount: uint256, @@ -29,23 +31,39 @@ def test_add_market(controller_factory, collateral_token, price_oracle, monetary with boa.env.anchor(): with boa.env.prank(admin): controller_factory.add_market( - collateral_token.address, 100, 10**16, 0, + collateral_token.address, + 100, + 10**16, + 0, price_oracle.address, - monetary_policy.address, 5 * 10**16, 2 * 10**16, - 10**8 * 10**18) + monetary_policy.address, + 5 * 10**16, + 2 * 10**16, + 10**8 * 10**18, + ) assert controller_factory.n_collaterals() == 1 - assert controller_factory.collaterals(0).lower() == collateral_token.address.lower() + assert ( + controller_factory.collaterals(0).lower() + == collateral_token.address.lower() + ) - controller = controller_interface.at(controller_factory.get_controller(collateral_token.address)) - amm = amm_interface.at(controller_factory.get_amm(collateral_token.address)) + controller = LEND_CONTROLLER_DEPLOYER.at( + controller_factory.get_controller(collateral_token.address) + ) + amm = AMM_DEPLOYER.at(controller_factory.get_amm(collateral_token.address)) assert controller.factory().lower() == controller_factory.address.lower() - assert controller.collateral_token().lower() == collateral_token.address.lower() + assert ( + controller.collateral_token().lower() + == collateral_token.address.lower() + ) assert controller.amm().lower() == amm.address.lower() - assert controller.monetary_policy().lower() == monetary_policy.address.lower() + assert ( + controller.monetary_policy().lower() == monetary_policy.address.lower() + ) assert controller.liquidation_discount() == 2 * 10**16 - assert controller.loan_discount() == 5 * 10**16 + assert controller.loan_discount() == 5 * 10**16 assert controller_factory.debt_ceiling(controller) == 10**8 * 10**18 assert amm.admin().lower() == controller.address.lower() diff --git a/tests/stableborrow/test_leverage.py b/tests/stableborrow/test_leverage.py index 564fa346..b9695e55 100644 --- a/tests/stableborrow/test_leverage.py +++ b/tests/stableborrow/test_leverage.py @@ -1,101 +1,167 @@ import boa -from ..conftest import approx +import pytest +from eth_abi import encode from hypothesis import given, settings from hypothesis import strategies as st -def test_leverage(collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts): +def test_leverage( + collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts +): user = accounts[0] amount = 10 * 10**18 controller_mint = stablecoin.balanceOf(market_controller.address) with boa.env.prank(user): - collateral_token._mint_for_testing(user, amount) - - market_controller.create_loan_extended(amount, amount * 2 * 3000, 5, fake_leverage.address, [int(amount * 1.5)]) - assert collateral_token.balanceOf(user) == 0 + boa.deal(collateral_token, user, amount) + + calldata = encode(["uint256"], [int(amount * 1.5)]) + fake_leverage_balances1 = [ + stablecoin.balanceOf(fake_leverage), + collateral_token.balanceOf(fake_leverage), + ] + market_controller.create_loan( + amount, amount * 2 * 3000, 5, user, fake_leverage.address, calldata + ) + fake_leverage_balances2 = [ + stablecoin.balanceOf(fake_leverage), + collateral_token.balanceOf(fake_leverage), + ] assert collateral_token.balanceOf(market_amm.address) == 3 * amount assert market_amm.get_sum_xy(user) == [0, 3 * amount] assert market_controller.debt(user) == amount * 2 * 3000 assert stablecoin.balanceOf(user) == 0 - - market_controller.repay_extended(fake_leverage.address, [10**18]) + assert collateral_token.balanceOf(user) == 0 + assert ( + fake_leverage_balances2[0] - fake_leverage_balances1[0] == amount * 2 * 3000 + ), 5 + assert fake_leverage_balances1[1] - fake_leverage_balances2[1] == 2 * amount + + calldata = encode(["uint256"], [10**18]) + market_controller.repay( + 0, user, market_amm.active_band(), fake_leverage.address, calldata + ) + fake_leverage_balances3 = [ + stablecoin.balanceOf(fake_leverage), + collateral_token.balanceOf(fake_leverage), + ] assert market_controller.debt(user) == 0 assert collateral_token.balanceOf(market_amm.address) == 0 assert stablecoin.balanceOf(market_amm.address) == 0 assert market_amm.get_sum_xy(user) == [0, 0] assert collateral_token.balanceOf(market_controller.address) == 0 assert stablecoin.balanceOf(market_controller.address) == controller_mint - assert collateral_token.balanceOf(user) == amount assert stablecoin.balanceOf(user) == 0 + assert collateral_token.balanceOf(user) == amount + assert fake_leverage_balances3[0] == fake_leverage_balances1[0] + assert fake_leverage_balances3[1] == fake_leverage_balances1[1] @given( amount=st.integers(min_value=10 * 10**8, max_value=10**18), loan_mul=st.floats(min_value=0, max_value=10.0), loan_more_mul=st.floats(min_value=0, max_value=10.0), - repay_mul=st.floats(min_value=0, max_value=1.0)) + repay_mul=st.floats(min_value=0, max_value=1.0), +) @settings(max_examples=2000) -def test_leverage_property(collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts, - amount, loan_mul, loan_more_mul, repay_mul): +def test_leverage_property( + collateral_token, + stablecoin, + market_controller, + market_amm, + fake_leverage, + accounts, + amount, + loan_mul, + loan_more_mul, + repay_mul, +): user = accounts[0] with boa.env.prank(user): - collateral_token._mint_for_testing(user, amount) + boa.deal(collateral_token, user, amount) debt = int(loan_mul * amount * 3000) - if (debt // 3000) <= collateral_token.balanceOf(fake_leverage.address) and debt > 0: - market_controller.create_loan_extended(amount, debt, 5, fake_leverage.address, [0]) + calldata = encode(["uint256"], [0]) + if (debt // 3000) <= collateral_token.balanceOf( + fake_leverage.address + ) and debt > 0: + market_controller.create_loan( + amount, debt, 5, user, fake_leverage.address, calldata + ) else: with boa.reverts(): - market_controller.create_loan_extended(amount, debt, 5, fake_leverage.address, [0]) + market_controller.create_loan( + amount, debt, 5, user, fake_leverage.address, calldata + ) return assert collateral_token.balanceOf(user) == 0 expected_collateral = int((1 + loan_mul) * amount) - assert approx(collateral_token.balanceOf(market_amm.address), expected_collateral, 1e-9, 10) + assert collateral_token.balanceOf(market_amm.address) == pytest.approx( + expected_collateral, rel=1e-9, abs=10 + ) xy = market_amm.get_sum_xy(user) assert xy[0] == 0 - assert approx(xy[1], expected_collateral, 1e-9, 10) - assert approx(market_controller.debt(user), debt, 1e-9, 10) + assert xy[1] == pytest.approx(expected_collateral, rel=1e-9, abs=10) + assert market_controller.debt(user) == pytest.approx(debt, rel=1e-9, abs=10) assert stablecoin.balanceOf(user) == 0 more_debt = int(loan_more_mul * amount * 3000) if (more_debt // 3000) <= collateral_token.balanceOf(fake_leverage.address): if more_debt > 0: - collateral_token._mint_for_testing(user, amount) - market_controller.borrow_more_extended(amount, more_debt, fake_leverage.address, [0]) + boa.deal(collateral_token, user, amount) + market_controller.borrow_more( + amount, more_debt, user, fake_leverage.address, calldata + ) debt += more_debt if more_debt > 0: assert collateral_token.balanceOf(user) == 0 expected_collateral = int((2 + loan_mul + loan_more_mul) * amount) - assert approx(collateral_token.balanceOf(market_amm.address), expected_collateral, 1e-9, 10) + assert collateral_token.balanceOf(market_amm.address) == pytest.approx( + expected_collateral, rel=1e-9, abs=10 + ) xy = market_amm.get_sum_xy(user) assert xy[0] == 0 - assert approx(xy[1], expected_collateral, 1e-9, 10) - assert approx(market_controller.debt(user), debt, 1e-9, 10) + assert xy[1] == pytest.approx(expected_collateral, rel=1e-9, abs=10) + assert market_controller.debt(user) == pytest.approx( + debt, rel=1e-9, abs=10 + ) assert stablecoin.balanceOf(user) == 0 else: with boa.reverts(): - market_controller.borrow_more_extended(amount, more_debt, fake_leverage.address, [0]) + market_controller.borrow_more( + amount, more_debt, user, fake_leverage.address, calldata + ) s0 = stablecoin.balanceOf(market_controller.address) + calldata = encode(["uint256"], [int(repay_mul * 1e18)]) if debt * int(repay_mul * 1e18) // 10**18 >= 1: - market_controller.repay_extended(fake_leverage.address, [int(repay_mul * 1e18)]) + market_controller.repay( + 0, user, market_amm.active_band(), fake_leverage.address, calldata + ) else: with boa.reverts(): - market_controller.repay_extended(fake_leverage.address, [int(repay_mul * 1e18)]) + market_controller.repay( + 0, user, market_amm.active_band(), fake_leverage.address, calldata + ) return - assert market_controller.debt(user) == debt - debt * int(repay_mul * 1e18) // 10**18 + assert ( + market_controller.debt(user) + == debt - debt * int(repay_mul * 1e18) // 10**18 + ) if repay_mul == 1.0: assert collateral_token.balanceOf(market_amm.address) == 0 assert stablecoin.balanceOf(market_amm.address) == 0 assert market_amm.get_sum_xy(user) == [0, 0] assert collateral_token.balanceOf(market_controller.address) == 0 - assert stablecoin.balanceOf(market_controller.address) - s0 == debt * int(repay_mul * 1e18) // 10**18 + assert ( + stablecoin.balanceOf(market_controller.address) - s0 + == debt * int(repay_mul * 1e18) // 10**18 + ) if repay_mul == 1.0: if more_debt > 0: assert abs(collateral_token.balanceOf(user) - 2 * amount) <= 1 @@ -105,12 +171,35 @@ def test_leverage_property(collateral_token, stablecoin, market_controller, mark assert collateral_token.balanceOf(user) == 0 -def test_deleverage_error(collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts): +def test_deleverage_error( + collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts +): test_leverage_property.hypothesis.inner_test( - collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts, 1000000000, 1.0, 0, 0.5) - - -def test_no_coins_to_repay(collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts): + collateral_token, + stablecoin, + market_controller, + market_amm, + fake_leverage, + accounts, + 1000000000, + 1.0, + 0, + 0.5, + ) + + +def test_no_coins_to_repay( + collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts +): test_leverage_property.hypothesis.inner_test( - collateral_token, stablecoin, market_controller, market_amm, fake_leverage, accounts, 128102389400761, 2.0, - 0, 1.3010426069826053e-18) + collateral_token, + stablecoin, + market_controller, + market_amm, + fake_leverage, + accounts, + 128102389400761, + 2.0, + 0, + 1.3010426069826053e-18, + ) diff --git a/tests/stableborrow/test_liquidate.py b/tests/stableborrow/test_liquidate.py index 7124da03..45ccf05d 100644 --- a/tests/stableborrow/test_liquidate.py +++ b/tests/stableborrow/test_liquidate.py @@ -3,15 +3,22 @@ from boa import BoaError from hypothesis import given, settings from hypothesis import strategies as st -from ..conftest import approx N = 5 @pytest.fixture(scope="module") -def controller_for_liquidation(stablecoin, collateral_token, market_controller, market_amm, - price_oracle, monetary_policy, admin, accounts): +def controller_for_liquidation( + stablecoin, + collateral_token, + market_controller, + market_amm, + price_oracle, + monetary_policy, + admin, + accounts, +): def f(sleep_time, discount): user = admin user2 = accounts[2] @@ -20,14 +27,14 @@ def f(sleep_time, discount): with boa.env.prank(admin): market_controller.set_amm_fee(10**6) monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY - collateral_token._mint_for_testing(user, collateral_amount) - collateral_token._mint_for_testing(user2, collateral_amount) - stablecoin.approve(market_amm, 2**256-1) - stablecoin.approve(market_controller, 2**256-1) - collateral_token.approve(market_controller, 2**256-1) + boa.deal(collateral_token, user, collateral_amount) + boa.deal(collateral_token, user2, collateral_amount) + stablecoin.approve(market_amm, 2**256 - 1) + stablecoin.approve(market_controller, 2**256 - 1) + collateral_token.approve(market_controller, 2**256 - 1) with boa.env.prank(user2): - collateral_token.approve(market_controller, 2**256-1) - debt = market_controller.max_borrowable(collateral_amount, N) + collateral_token.approve(market_controller, 2**256 - 1) + debt = market_controller.max_borrowable(collateral_amount, N) * 99 // 100 with boa.env.prank(user): market_controller.create_loan(collateral_amount, debt, N) @@ -55,7 +62,9 @@ def f(sleep_time, discount): market_controller.collect_fees() # Check that we earned the same in admin fees as we need to liquidate # Calculation is not precise because of dead shares, but the last withdrawal will put dust in admin fees - assert approx(stablecoin.balanceOf(fee_receiver), market_controller.tokens_to_liquidate(user), 1e-10) + assert stablecoin.balanceOf(fee_receiver) == pytest.approx( + market_controller.tokens_to_liquidate(user), rel=1e-10 + ) # Borrow some more funds to repay for our overchargings with DEAD_SHARES with boa.env.prank(user2): @@ -82,15 +91,30 @@ def test_liquidate(accounts, admin, controller_for_liquidation, market_amm, stab controller.liquidate(user, int(x * 0.999999)) -@given(frac=st.integers(min_value=0, max_value=11 * 10**17)) +@given(frac=st.integers(min_value=0, max_value=10**18)) @settings(max_examples=200) -def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, controller_for_liquidation, market_amm, fake_leverage, frac): +def test_liquidate_callback( + accounts, + admin, + stablecoin, + collateral_token, + controller_for_liquidation, + market_amm, + fake_leverage, + frac, +): user = admin fee_receiver = accounts[0] ld = int(0.02 * 1e18) if frac < 10**18: # f = ((1 + h/2) / (1 + h) * (1 - frac) + frac) * frac - f = ((10 ** 18 + ld // 2) * (10 ** 18 - frac) // (10 ** 18 + ld) + frac) * frac // 10 ** 18 // 5 * 5 # The latter part is rounding off for multiple bands + f = ( + ((10**18 + ld // 2) * (10**18 - frac) // (10**18 + ld) + frac) + * frac + // 10**18 + // 5 + * 5 + ) # The latter part is rounding off for multiple bands else: f = 10**18 # Partial liquidation improves health. @@ -110,8 +134,8 @@ def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, contr # we do it by borrowing if f != 10**18: with boa.env.prank(fee_receiver): - collateral_token._mint_for_testing(fee_receiver, 10**18) - collateral_token.approve(controller.address, 2**256-1) + boa.deal(collateral_token, fee_receiver, 10**18) + collateral_token.approve(controller.address, 2**256 - 1) debt2 = controller.max_borrowable(10**18, 5) controller.create_loan(10**18, debt2, 5) stablecoin.transfer(fake_leverage.address, debt2) @@ -121,10 +145,12 @@ def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, contr b = stablecoin.balanceOf(fee_receiver) stablecoin.transfer(fake_leverage.address, b) health_before = controller.health(user) + try: dy = collateral_token.balanceOf(fee_receiver) - controller.liquidate_extended(user, int(0.999 * f * x / 1e18), frac, - fake_leverage.address, []) + controller.liquidate( + user, int(0.999 * f * x / 1e18), frac, fake_leverage.address, b"" + ) dy = collateral_token.balanceOf(fee_receiver) - dy dx = stablecoin.balanceOf(fee_receiver) - b if f > 0: @@ -141,12 +167,16 @@ def test_liquidate_callback(accounts, admin, stablecoin, collateral_token, contr raise -def test_self_liquidate(accounts, admin, controller_for_liquidation, market_amm, stablecoin): +def test_self_liquidate( + accounts, admin, controller_for_liquidation, market_amm, stablecoin +): user = admin fee_receiver = accounts[0] with boa.env.anchor(): - controller = controller_for_liquidation(sleep_time=40 * 86400, discount=2.5 * 10**16) + controller = controller_for_liquidation( + sleep_time=40 * 86400, discount=2.5 * 10**16 + ) with boa.env.prank(accounts[2]): stablecoin.transfer(fee_receiver, 10**10) @@ -167,24 +197,27 @@ def test_self_liquidate(accounts, admin, controller_for_liquidation, market_amm, @given(frac=st.integers(min_value=10**14, max_value=10**18 - 13)) -def test_tokens_to_liquidate(accounts, admin, controller_for_liquidation, market_amm, stablecoin, frac): +def test_tokens_to_liquidate( + accounts, admin, controller_for_liquidation, market_amm, stablecoin, frac +): user = admin fee_receiver = accounts[0] with boa.env.anchor(): controller = controller_for_liquidation(sleep_time=80 * 86400, discount=0) - initial_balance = stablecoin.balanceOf(fee_receiver) tokens_to_liquidate = controller.tokens_to_liquidate(user, frac) with boa.env.prank(accounts[2]): stablecoin.transfer(fee_receiver, 10**10) + initial_balance = stablecoin.balanceOf(fee_receiver) with boa.env.prank(fee_receiver): - controller.liquidate_extended(user, 0, frac, "0x0000000000000000000000000000000000000000", []) + controller.liquidate(user, 0, frac) balance = stablecoin.balanceOf(fee_receiver) + spent = initial_balance - balance if frac < 10**18: - assert approx(balance, initial_balance - tokens_to_liquidate, 1e5, abs_precision=1e5) + assert spent == pytest.approx(tokens_to_liquidate, rel=1e5, abs=1e5) else: assert balance != initial_balance - tokens_to_liquidate diff --git a/tests/stableborrow/test_lm_callback.py b/tests/stableborrow/test_lm_callback.py index ead4f74e..4da1ada8 100644 --- a/tests/stableborrow/test_lm_callback.py +++ b/tests/stableborrow/test_lm_callback.py @@ -1,19 +1,21 @@ import boa import pytest from collections import defaultdict -from ..conftest import approx +from tests.utils.deployers import DUMMY_LM_CALLBACK_DEPLOYER @pytest.fixture(scope="module") def lm_callback(market_amm, market_controller, admin): with boa.env.prank(admin): - cb = boa.load('contracts/testing/DummyLMCallback.vy', market_amm.address) + cb = DUMMY_LM_CALLBACK_DEPLOYER.deploy(market_amm.address) market_controller.set_callback(cb.address) return cb -@pytest.mark.skip('Need to update the mock') -def test_lm_callback(collateral_token, lm_callback, market_amm, market_controller, accounts): +@pytest.mark.skip("Need to update the mock") +def test_lm_callback( + collateral_token, lm_callback, market_amm, market_controller, accounts +): """ This unitary test doesn't do trades etc - that has to be done in a full stateful test """ @@ -21,7 +23,7 @@ def test_lm_callback(collateral_token, lm_callback, market_amm, market_controlle debt = 5 * 10**18 * 3000 for i, acc in enumerate(accounts[:10]): with boa.env.prank(acc): - collateral_token._mint_for_testing(acc, amount) + boa.deal(collateral_token, acc, amount) market_controller.create_loan(amount, debt, 5 + i) user_amounts = defaultdict(int) @@ -32,4 +34,6 @@ def test_lm_callback(collateral_token, lm_callback, market_amm, market_controlle user_amounts[acc] += cps * us // 10**18 for acc in accounts[:10]: - assert approx(user_amounts[acc], market_amm.get_sum_xy(acc)[1], 1e-5) + assert user_amounts[acc] == pytest.approx( + market_amm.get_sum_xy(acc)[1], rel=1e-5 + ) diff --git a/tests/stableborrow/test_st_interest_conservation.py b/tests/stableborrow/test_st_interest_conservation.py index caf9b068..2fbb61e0 100644 --- a/tests/stableborrow/test_st_interest_conservation.py +++ b/tests/stableborrow/test_st_interest_conservation.py @@ -1,30 +1,39 @@ import boa from hypothesis import settings from hypothesis import strategies as st -from hypothesis.stateful import RuleBasedStateMachine, run_state_machine_as_test, rule, invariant +from hypothesis.stateful import ( + RuleBasedStateMachine, + run_state_machine_as_test, + rule, + invariant, +) -DEAD_SHARES = 1000 +from tests.utils.constants import DEAD_SHARES class StatefulLendBorrow(RuleBasedStateMachine): n = st.integers(min_value=5, max_value=50) - amount = st.integers(min_value=0, max_value=2**256-1) - c_amount = st.integers(min_value=0, max_value=2**256-1) + amount = st.integers(min_value=0, max_value=2**256 - 1) + c_amount = st.integers(min_value=0, max_value=2**256 - 1) user_id = st.integers(min_value=0, max_value=9) t = st.integers(min_value=0, max_value=86400 * 365) - rate = st.integers(min_value=0, max_value=2**255 - 1) # Negative is probably not good btw + rate = st.integers( + min_value=0, max_value=2**255 - 1 + ) # Negative is probably not good btw def __init__(self): super().__init__() self.collateral = self.collateral_token self.amm = self.market_amm self.controller = self.market_controller - self.debt_ceiling = self.controller_factory.debt_ceiling(self.controller.address) + self.debt_ceiling = self.controller_factory.debt_ceiling( + self.controller.address + ) for u in self.accounts: with boa.env.prank(u): - self.collateral_token.approve(self.controller.address, 2**256-1) - self.stablecoin.approve(self.controller.address, 2**256-1) + self.collateral_token.approve(self.controller.address, 2**256 - 1) + self.stablecoin.approve(self.controller.address, 2**256 - 1) with boa.env.prank(self.admin): self.monetary_policy.set_rate(int(1e18 * 0.04 / 365 / 86400)) @@ -34,7 +43,7 @@ def create_loan(self, c_amount, amount, n, user_id): with boa.env.prank(user): if self.controller.loan_exists(user): - with boa.reverts('Loan already created'): + with boa.reverts("Loan already created"): self.controller.create_loan(c_amount, amount, n) return @@ -42,16 +51,17 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.calculate_debt_n1(c_amount, amount, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.create_loan(c_amount, amount, n) return if self.controller.total_debt() + amount > self.debt_ceiling: if ( - (self.controller.total_debt() + amount) * self.amm.get_rate_mul() > 2**256 - 1 - or c_amount * self.amm.get_p() > 2**256 - 1 + (self.controller.total_debt() + amount) * self.amm.get_rate_mul() + > 2** 256 - 1 + or c_amount * self.amm.get_p() > 2** 256 - 1 ): with boa.reverts(): self.controller.create_loan(c_amount, amount, n) @@ -61,13 +71,13 @@ def create_loan(self, c_amount, amount, n, user_id): return if amount == 0: - with boa.reverts('No loan'): + with boa.reverts("No loan"): self.controller.create_loan(c_amount, amount, n) # It's actually division by zero which happens return try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -81,7 +91,9 @@ def create_loan(self, c_amount, amount, n, user_id): self.controller.create_loan(c_amount, amount, n) return except Exception as e: - if ('Too deep' in str(e) and c_amount * 3000 / amount < 1e-3) or 'Amount too low' in str(e): + if ( + "Too deep" in str(e) and c_amount * 3000 / amount < 1e-3 + ) or "Amount too low" in str(e): return else: raise @@ -89,7 +101,7 @@ def create_loan(self, c_amount, amount, n, user_id): try: self.controller.create_loan(c_amount, amount, n) except Exception as e: - if 'Too deep' in str(e) and c_amount * 3000 / amount < 1e-3: + if "Too deep" in str(e) and c_amount * 3000 / amount < 1e-3: pass else: if c_amount // n <= (2**128 - 1) // DEAD_SHARES: @@ -101,7 +113,6 @@ def repay(self, amount, user_id): with boa.env.prank(user): if amount == 0: - self.controller.repay(amount, user) return if not self.controller.loan_exists(user): @@ -120,7 +131,13 @@ def repay(self, amount, user_id): with boa.env.prank(self.accounts[0]): self.stablecoin.transfer(user, min(diff, admin_balance)) if user_balance + admin_balance < min(amount, user_debt): - assert sum(sum(abs(n) for n in self.amm.read_user_tick_numbers(u)) > 0 for u in self.accounts[:10]) > 1 + assert ( + sum( + sum(abs(n) for n in self.amm.read_user_tick_numbers(u)) > 0 + for u in self.accounts[:10] + ) + > 1 + ) return self.controller.repay(amount, user) @@ -131,7 +148,7 @@ def add_collateral(self, c_amount, user_id): with boa.env.prank(user): try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -144,7 +161,9 @@ def add_collateral(self, c_amount, user_id): self.controller.add_collateral(c_amount, user) return - if (c_amount + self.amm.get_sum_xy(user)[1]) * self.amm.get_p() > 2**256 - 1: + if ( + c_amount + self.amm.get_sum_xy(user)[1] + ) * self.amm.get_p() > 2**256 - 1: with boa.reverts(): self.controller.add_collateral(c_amount, user) return @@ -152,7 +171,9 @@ def add_collateral(self, c_amount, user_id): try: self.controller.add_collateral(c_amount, user) except Exception: - if (c_amount + self.amm.get_sum_xy(user)[1]) < (2**128 - 1) // DEAD_SHARES: + if (c_amount + self.amm.get_sum_xy(user)[1]) < ( + 2**128 - 1 + ) // DEAD_SHARES: raise @rule(c_amount=c_amount, amount=amount, user_id=user_id) @@ -165,7 +186,7 @@ def borrow_more(self, c_amount, amount, user_id): return try: - self.collateral._mint_for_testing(user, c_amount) + boa.deal(self.collateral, user, c_amount) except Exception: return # Probably overflow @@ -185,14 +206,16 @@ def borrow_more(self, c_amount, amount, user_id): try: self.controller.calculate_debt_n1(final_collateral, final_debt, n) except Exception as e: - too_high = 'Debt too high' in str(e) + too_high = "Debt too high" in str(e) if too_high: - with boa.reverts('Debt too high'): + with boa.reverts("Debt too high"): self.controller.borrow_more(c_amount, amount) return if self.controller.total_debt() + amount > self.debt_ceiling: - if (self.controller.total_debt() + amount) * self.amm.get_rate_mul() > 2**256 - 1: + if ( + self.controller.total_debt() + amount + ) * self.amm.get_rate_mul() > 2**256 - 1: with boa.reverts(): self.controller.borrow_more(c_amount, amount) else: @@ -218,7 +241,10 @@ def change_rate(self, rate): @invariant() def sum_of_debts(self): - assert abs(sum(self.controller.debt(u) for u in self.accounts) - self.controller.total_debt()) <= len(self.accounts) + assert abs( + sum(self.controller.debt(u) for u in self.accounts) + - self.controller.total_debt() + ) <= len(self.accounts) @invariant() def debt_payable(self): @@ -230,25 +256,56 @@ def debt_payable(self): assert debt == supply - b -def test_stateful_lendborrow(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): - StatefulLendBorrow.TestCase.settings = settings(max_examples=200, stateful_step_count=10) +def test_stateful_lendborrow( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): + StatefulLendBorrow.TestCase.settings = settings( + max_examples=200, stateful_step_count=10 + ) for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) run_state_machine_as_test(StatefulLendBorrow) -def test_rate_too_high(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_rate_too_high( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): state = StatefulLendBorrow() - state.change_rate(rate=19298681539552733520784193015473224553355594960504706685844695763378761203935) + state.change_rate( + rate=19298681539552733520784193015473224553355594960504706685844695763378761203935 + ) state.time_travel(100000) # rate clipping state.amm.get_rate_mul() -def test_unexpected_revert(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_unexpected_revert( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): @@ -258,7 +315,16 @@ def test_unexpected_revert(controller_factory, market_amm, market_controller, mo state.repay(amount=39777, user_id=1) -def test_no_revert_reason(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_no_revert_reason( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): @@ -266,17 +332,37 @@ def test_no_revert_reason(controller_factory, market_amm, market_controller, mon state.create_loan(amount=1, c_amount=1, n=5, user_id=0) -def test_too_deep(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_too_deep( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) with boa.env.anchor(): state = StatefulLendBorrow() state.create_loan(amount=13119, c_amount=48, n=43, user_id=0) state.create_loan(amount=34049, c_amount=48388, n=18, user_id=1) - state.create_loan(amount=10161325728155098164, c_amount=4156800770, n=50, user_id=2) - - -def test_overflow(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): + state.create_loan( + amount=10161325728155098164, c_amount=4156800770, n=50, user_id=2 + ) + + +def test_overflow( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() @@ -288,7 +374,11 @@ def test_overflow(controller_factory, market_amm, market_controller, monetary_po state.time_travel(t=0) state.debt_payable() state.sum_of_debts() - state.borrow_more(amount=102598604287098624639570500830661495567, c_amount=12171265979771193868, user_id=0) + state.borrow_more( + amount=102598604287098624639570500830661495567, + c_amount=12171265979771193868, + user_id=0, + ) state.debt_payable() state.sum_of_debts() state.create_loan(amount=1, c_amount=5295, n=5, user_id=0) @@ -301,7 +391,16 @@ def test_overflow(controller_factory, market_amm, market_controller, monetary_po state.teardown() -def test_cannot_repay_1(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_cannot_repay_1( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() @@ -323,7 +422,16 @@ def test_cannot_repay_1(controller_factory, market_amm, market_controller, monet state.teardown() -def test_borrow_more_0(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_borrow_more_0( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() @@ -339,7 +447,16 @@ def test_borrow_more_0(controller_factory, market_amm, market_controller, moneta state.teardown() -def test_no_coins(controller_factory, market_amm, market_controller, monetary_policy, collateral_token, stablecoin, accounts, admin): +def test_no_coins( + controller_factory, + market_amm, + market_controller, + monetary_policy, + collateral_token, + stablecoin, + accounts, + admin, +): for k, v in locals().items(): setattr(StatefulLendBorrow, k, v) state = StatefulLendBorrow() diff --git a/tests/swap/conftest.py b/tests/swap/conftest.py deleted file mode 100644 index a1932484..00000000 --- a/tests/swap/conftest.py +++ /dev/null @@ -1,58 +0,0 @@ -import boa -import pytest -from boa.interpret import VyperContract - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" - - -@pytest.fixture(scope="session") -def swap_impl(admin): - with boa.env.prank(admin): - return boa.load('contracts/Stableswap.vy') - - -@pytest.fixture(scope="session") -def swap_deployer(swap_impl, admin): - with boa.env.prank(admin): - deployer = boa.load('contracts/testing/SwapFactory.vy', swap_impl.address) - return deployer - - -@pytest.fixture(scope="session") -def redeemable_coin(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20Mock.vy', "Unbranded Redeemable USD", "urUSD", 6) - - -@pytest.fixture(scope="session") -def volatile_coin(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/ERC20Mock.vy', "Volatile USD", "vUSD", 18) - - -@pytest.fixture(scope="session") -def swap(swap_deployer, swap_impl, redeemable_coin, volatile_coin, admin): - with boa.env.prank(admin): - n = swap_deployer.n() - swap_deployer.deploy(redeemable_coin, volatile_coin) - addr = swap_deployer.pools(n) - swap = VyperContract( - swap_impl.compiler_data, - override_address=addr - ) - return swap - - -@pytest.fixture(scope="session") -def swap_w_d(swap, redeemable_coin, volatile_coin, accounts, admin): - with boa.env.prank(admin): - redeemable_coin._mint_for_testing(admin, 10**6 * 10**6) - volatile_coin._mint_for_testing(admin, 10**6 * 10**18) - redeemable_coin.approve(swap.address, 2**256 - 1) - volatile_coin.approve(swap.address, 2**256 - 1) - swap.add_liquidity([10**6 * 10**6, 10**6 * 10**18], 0) - for acc in accounts: - with boa.env.prank(acc): - redeemable_coin.approve(swap.address, 2**256 - 1) - volatile_coin.approve(swap.address, 2**256 - 1) - return swap diff --git a/tests/swap/test_price.py b/tests/swap/test_price.py deleted file mode 100644 index 79386af3..00000000 --- a/tests/swap/test_price.py +++ /dev/null @@ -1,47 +0,0 @@ -import boa -from hypothesis import given, settings -from hypothesis import strategies as st -from math import exp -from ..conftest import approx - - -@given( - amount=st.integers(min_value=1, max_value=10**6), - ix=st.integers(min_value=0, max_value=1)) -@settings(max_examples=100) -def test_price(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix): - user = accounts[0] - assert swap_w_d.get_p() == 10**18 - from_coin = [redeemable_coin, volatile_coin][ix] - amount *= 10**(from_coin.decimals()) - with boa.env.prank(user): - from_coin._mint_for_testing(user, amount) - swap_w_d.exchange(ix, 1-ix, amount, 0) - dy = swap_w_d.get_dy(0, 1, 10**6) - p1 = 10**18 / dy - p2 = swap_w_d.get_p() / 1e18 - assert approx(p1, p2, 0.04e-2 * 1.2) - - -@given( - amount=st.integers(min_value=1, max_value=10**5), - ix=st.integers(min_value=0, max_value=1), - dt0=st.integers(min_value=0, max_value=10**6), - dt=st.integers(min_value=0, max_value=10**6)) -@settings(max_examples=1000) -def test_ema(swap_w_d, redeemable_coin, volatile_coin, accounts, amount, ix, dt0, dt): - user = accounts[0] - from_coin = [redeemable_coin, volatile_coin][ix] - amount *= 10**(from_coin.decimals()) - with boa.env.prank(user): - from_coin._mint_for_testing(user, amount) - boa.env.time_travel(dt0) - swap_w_d.exchange(ix, 1-ix, amount, 0) - # Time didn't pass yet - p = swap_w_d.get_p() - assert approx(swap_w_d.last_price(), p, 1e-5) - assert approx(swap_w_d.price_oracle(), 10**18, 1e-5) - boa.env.time_travel(dt) - w = exp(-dt / 866) - p1 = int(10**18 * w + p * (1 - w)) - assert approx(swap_w_d.price_oracle(), p1, 1e-5) diff --git a/tests/test_math.py b/tests/test_math.py deleted file mode 100644 index 27488df0..00000000 --- a/tests/test_math.py +++ /dev/null @@ -1,96 +0,0 @@ -import pytest -import boa -from math import log2, sqrt, exp, log -from hypothesis import given, settings -from hypothesis import strategies as st - -SETTINGS = dict(max_examples=2000) - - -@pytest.fixture(scope="module") -def optimized_math(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/OptimizeMath.vy') - - -@given(st.integers(min_value=0, max_value=2**256-1)) -@settings(**SETTINGS) -def test_log2(optimized_math, x): - y1 = optimized_math.original_log2(x) - if x > 0: - y2 = optimized_math.optimized_log2(x) - else: - with pytest.raises(Exception): - optimized_math.optimized_log2(x) - y2 = 0 - if x > 0: - y = log2(x / 1e18) - else: - y = 0 - - if x >= 10**18: - assert y1 == y2 - else: - assert y1 == 0 - assert abs(y2 / 1e18 - y) <= max(1e-9, 1e-9 * (abs(y) + 1)) - - -@given(st.integers(min_value=0, max_value=2**256-1)) -@settings(**SETTINGS) -def test_sqrt(optimized_math, x): - if x > (2**256 - 1) // 10**18: - with boa.reverts(): - optimized_math.original_sqrt(x) - with boa.reverts(): - optimized_math.optimized_sqrt(x) - return - - y1 = optimized_math.original_sqrt(x) - y2 = optimized_math.optimized_sqrt(x) - y = sqrt(x / 1e18) - - assert y1 == y2 - assert abs(y2 / 1e18 - y) <= max(1e-15, 1e-15 * y) - - -@given(st.integers(min_value=0, max_value=2**256-1)) -@settings(**SETTINGS) -def test_halfpow(optimized_math, power): - pow_int = optimized_math.halfpow(power) / 1e18 - pow_ideal = 0.5 ** (power / 1e18) - assert abs(pow_int - pow_ideal) < max(5 * 1e10 / 1e18, 5e-16) - - -@given(st.integers(min_value=-2**255, max_value=2**254-1)) -@settings(**SETTINGS) -def test_exp(optimized_math, power): - if power >= 135305999368893231589: - with boa.reverts("exp overflow"): - optimized_math.optimized_exp(power) - elif power <= -41446531673892821376: - assert optimized_math.optimized_exp(power) == 0 - else: - pow_int = optimized_math.optimized_exp(power) - pow_ideal = int(exp(power / 1e18) * 1e18) - assert abs(pow_int - pow_ideal) < max(1e8, pow_ideal * 1e-10) - - -@given(st.integers(min_value=0, max_value=2**256-1)) -@settings(**SETTINGS) -def test_wad_ln(optimized_math, x): - if x > 0 and x < 2**255: - y_v = optimized_math.wad_ln(x) - elif x >= 2**255: - with boa.reverts(): - optimized_math.wad_ln(x) - return - else: - with boa.reverts(): - optimized_math.wad_ln(x) - y_v = 0 - if x > 0: - y = log(x / 1e18) - else: - y = 0 - - assert abs(y_v / 1e18 - y) <= max(1e-9, 1e-9 * (abs(y) + 1)) diff --git a/tests/test_packing.py b/tests/test_packing.py deleted file mode 100644 index 55c4b14f..00000000 --- a/tests/test_packing.py +++ /dev/null @@ -1,25 +0,0 @@ -import boa -import pytest -from hypothesis import strategies as st -from hypothesis import given, settings - -MAX_N = 2**127 - 1 -MIN_N = -2**127 + 1 # <- not -2**127! - - -@pytest.fixture(scope="module") -def packing(admin): - with boa.env.prank(admin): - return boa.load('contracts/testing/TestPacking.vy') - - -@given( - n1=st.integers(min_value=MIN_N, max_value=MAX_N), - n2=st.integers(min_value=MIN_N, max_value=MAX_N) -) -@settings(max_examples=500) -def test_packing(packing, n1, n2): - n1, n2 = sorted([n1, n2]) - n1out, n2out = packing.unpack_ticks(packing.pack_ticks(n1, n2)) - assert n1out == n1 - assert n2out == n2 diff --git a/tests/unitary/controller/test_internal_remove_from_list.py b/tests/unitary/controller/test_internal_remove_from_list.py new file mode 100644 index 00000000..5306d0b2 --- /dev/null +++ b/tests/unitary/controller/test_internal_remove_from_list.py @@ -0,0 +1,70 @@ +import boa +import pytest +from textwrap import dedent + +from tests.utils import max_approve + +COLLATERAL = 10**21 +DEBT = 10**18 +N_BANDS = 6 + + +@pytest.fixture(scope="module", autouse=True) +def expose_internal(controller): + controller.inject_function( + dedent( + """ + @external + def remove_from_list(_for: address): + core._remove_from_list(_for) + """ + ) + ) + + +def open_loans(controller, collateral_token, borrowers): + assert controller.n_loans() == 0 + for index, borrower in enumerate(borrowers): + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller, sender=borrower) + controller.create_loan(COLLATERAL, DEBT, N_BANDS, sender=borrower) + assert controller.loans(index) == borrower + assert controller.loan_ix(borrower) == index + + +def test_default_behavior_single_entry(controller, collateral_token): + borrower = boa.env.generate_address() + open_loans(controller, collateral_token, [borrower]) + + controller.inject.remove_from_list(borrower) + + assert controller.n_loans() == 0 + assert controller.loan_ix(borrower) == 0 + + +def test_default_behavior_removing_last_entry(controller, collateral_token): + borrowers = [boa.env.generate_address() for _ in range(3)] + open_loans(controller, collateral_token, borrowers) + + controller.inject.remove_from_list(borrowers[-1]) + + assert controller.n_loans() == 2 + assert controller.loans(0) == borrowers[0] + assert controller.loans(1) == borrowers[1] + assert controller.loan_ix(borrowers[0]) == 0 + assert controller.loan_ix(borrowers[1]) == 1 + assert controller.loan_ix(borrowers[2]) == 0 + + +def test_default_behavior_swap_last_into_gap(controller, collateral_token): + borrowers = [boa.env.generate_address() for _ in range(3)] + open_loans(controller, collateral_token, borrowers) + + controller.inject.remove_from_list(borrowers[1]) + + assert controller.n_loans() == 2 + assert controller.loans(0) == borrowers[0] + assert controller.loans(1) == borrowers[2] + assert controller.loan_ix(borrowers[0]) == 0 + assert controller.loan_ix(borrowers[1]) == 0 + assert controller.loan_ix(borrowers[2]) == 1 diff --git a/tests/unitary/controller/test_internal_repay_full.py b/tests/unitary/controller/test_internal_repay_full.py new file mode 100644 index 00000000..b9086d81 --- /dev/null +++ b/tests/unitary/controller/test_internal_repay_full.py @@ -0,0 +1,922 @@ +import boa +import pytest +from textwrap import dedent + +from tests.utils import filter_logs, max_approve +from tests.utils.constants import ZERO_ADDRESS + +COLLATERAL = 10**17 +N_BANDS = 6 + + +@pytest.fixture(scope="module", autouse=True) +def expose_internal(controller): + controller.inject_function( + dedent( + """ + @external + def repay_full( + _for: address, + _d_debt: uint256, + _approval: bool, + _xy: uint256[2], + _cb: core.IController.CallbackData, + _callbacker: address + ): + core._repay_full(_for, _d_debt, _approval, _xy, _cb, _callbacker) + """ + ) + ) + + +@pytest.fixture(scope="module") +def snapshot(controller, amm, fake_leverage): + def fn(token, borrower, payer): + return { + "controller": token.balanceOf(controller), + "borrower": token.balanceOf(borrower), + "payer": token.balanceOf(payer), + "amm": token.balanceOf(amm), + "callback": token.balanceOf(fake_leverage), + } + + return fn + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_wallet( + controller, borrowed_token, collateral_token, amm, snapshot, different_payer +): + """ + Test full repayment using only wallet tokens (no soft-liquidation). + + Money Flow: DEBT (wallet) → Controller + xy[1] (AMM) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, debt) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # xy[0] == 0, works both with and without approval + with boa.env.anchor(): + controller.inject.repay_full( + borrower, debt, False, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy[0] == 0 + assert xy[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == collateral_to_borrower + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_payer == -debt + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == COLLATERAL + assert collateral_from_amm == -COLLATERAL + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + fake_leverage, + different_payer, +): + """ + Test full repayment using only callback tokens. + + Money Flow: cb.borrowed (callback) → Controller + cb.borrowed - DEBT (callback) → Borrower (excess) + cb.collateral (callback) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + # ================= Setup callback tokens ================= + + # Mock collateral withdraw from amm to callbacker and mint borrowed for callbacker + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, COLLATERAL, sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt + 1) + cb = (amm.active_band(), debt + 1, COLLATERAL // 2) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + # xy[0] == 0, works both with and without approval. + # In practice, it can't be called without approval since repay with callback requires it. + with boa.env.anchor(): + controller.inject.repay_full( + borrower, debt, False, xy_before, cb, fake_leverage.address + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, cb, fake_leverage.address + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_callbacker = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy[0] == 0 + assert xy[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == COLLATERAL + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_to_borrower == 1 + assert borrowed_from_callback == -(debt + 1) + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_borrower == COLLATERAL // 2 + assert collateral_from_callbacker == -(COLLATERAL // 2) + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_xy0( + controller, borrowed_token, collateral_token, amm, snapshot, different_payer +): + """ + Test full repayment using only AMM soft-liquidation (xy[0] >= DEBT). + + Money Flow: xy[0] (AMM) → Controller + xy[0] - DEBT (AMM) → Borrower (excess) + xy[1] (AMM) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt * 10_001 // 10_000) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt * 10_001 // 10_000, 0) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] > debt and xy_before[1] > 0 + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + with boa.reverts(): + controller.inject.repay_full( + borrower, debt, False, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy_after = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy_after[0] == 0 + assert xy_after[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == debt + assert repay_logs[0].collateral_decrease == xy_before[1] + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -xy_before[0] + assert borrowed_to_borrower == xy_before[0] - debt + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == xy_before[1] + assert collateral_from_amm == -xy_before[1] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_wallet_and_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + fake_leverage, + different_payer, +): + """ + Test full repayment using wallet + callback tokens. + + Money Flow: cb.borrowed (callback) + (DEBT - cb.borrowed) (wallet) → Controller + cb.collateral (callback) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, 1) + + # ================= Setup callback tokens ================= + + # Mock collateral withdraw from amm to callbacker and mint borrowed for callbacker + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, COLLATERAL, sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt - 1) + cb = (amm.active_band(), debt - 1, COLLATERAL // 2) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # xy[0] == 0, works both with and without approval. + # In practice, it can't be called without approval since repay with callback requires it. + with boa.env.anchor(): + controller.inject.repay_full( + borrower, debt, False, xy_before, cb, fake_leverage.address + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, cb, fake_leverage.address + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_callbacker = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy[0] == 0 + assert xy[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == COLLATERAL + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_payer == -1 + assert borrowed_from_callback == -(debt - 1) + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_borrower == COLLATERAL // 2 + assert collateral_from_callbacker == -(COLLATERAL // 2) + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_xy0_and_wallet( + controller, borrowed_token, collateral_token, amm, snapshot, different_payer +): + """ + Test full repayment using both AMM soft-liquidation (xy[0]) and wallet tokens. + + Money Flow: xy[0] (AMM) + (DEBT - xy[0]) (wallet) → Controller + xy[1] (AMM) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 and debt // 2 > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, debt) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + with boa.reverts(): + controller.inject.repay_full( + borrower, debt, False, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, (0, 0, 0), ZERO_ADDRESS + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy_after = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy_after[0] == 0 + assert xy_after[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == debt + assert repay_logs[0].collateral_decrease == xy_before[1] + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_payer == -(debt - xy_before[0]) + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == xy_before[1] + assert collateral_from_amm == -xy_before[1] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_xy0_and_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + fake_leverage, + different_payer, +): + """ + Test full repayment using AMM soft-liquidation + callback tokens. + + Money Flow: xy[0] (AMM) + cb.borrowed (callback) → Controller + xy[0] + cb.borrowed - DEBT (AMM) → Borrower (excess) + cb.collateral (callback) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 1 < xy_before[0] < debt and xy_before[1] > 0 + + # ================= Setup callback tokens ================= + + # Mock collateral withdraw from amm to callbacker and mint borrowed for callbacker + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, xy_before[1], sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt - 1) + cb = (amm.active_band(), debt - 1, xy_before[1] // 2) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + with boa.reverts(): + controller.inject.repay_full( + borrower, debt, False, xy_before, cb, fake_leverage.address + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, cb, fake_leverage.address + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_callbacker = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy[0] == 0 + assert xy[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == xy_before[1] + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_callback == -(debt - 1) + assert borrowed_to_borrower == xy_before[0] - 1 + + assert collateral_to_borrower == xy_before[1] // 2 + assert collateral_from_callbacker == -(xy_before[1] // 2) + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_full_from_wallet_and_xy0_and_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + fake_leverage, + different_payer, +): + """ + Test full repayment using all three sources: AMM + wallet + callback. + + Money Flow: xy[0] (AMM) + cb.borrowed (callback) + (DEBT - xy[0] - cb.borrowed) (wallet) → Controller + cb.collateral (callback) → Borrower + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 1 < xy_before[0] < debt and xy_before[1] > 0 + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, 1) + + # ================= Setup callback tokens ================= + + # Mock collateral withdraw from amm to callbacker and mint borrowed for callbacker + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, xy_before[1], sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt - xy_before[0] - 1) + cb = (amm.active_band(), debt - xy_before[0] - 1, xy_before[1] // 2) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + with boa.reverts(): + controller.inject.repay_full( + borrower, debt, False, xy_before, cb, fake_leverage.address + ) + controller.inject.repay_full( + borrower, debt, True, xy_before, cb, fake_leverage.address + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_callbacker = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + # Withdrawn from AMM + xy = amm.get_sum_xy(borrower) + assert amm.user_shares(borrower)[1][0] == 0 + assert xy[0] == 0 + assert xy[1] == 0 + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].collateral == 0 + assert state_logs[0].debt == 0 + assert state_logs[0].n1 == 0 + assert state_logs[0].n2 == 0 + assert state_logs[0].liquidation_discount == 0 + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == borrowed_to_controller + assert repay_logs[0].collateral_decrease == xy_before[1] + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_callback == -(debt - xy_before[0] - 1) + assert borrowed_from_payer == -1 + + assert collateral_to_borrower == xy_before[1] // 2 + assert collateral_from_callbacker == -(xy_before[1] // 2) + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] diff --git a/tests/unitary/controller/test_internal_repay_partial.py b/tests/unitary/controller/test_internal_repay_partial.py new file mode 100644 index 00000000..e0de6d6a --- /dev/null +++ b/tests/unitary/controller/test_internal_repay_partial.py @@ -0,0 +1,1457 @@ +import boa +import pytest +from textwrap import dedent + +from tests.utils import filter_logs, max_approve +from tests.utils.constants import ZERO_ADDRESS + +COLLATERAL = 10**17 +N_BANDS = 6 + + +@pytest.fixture(scope="module", autouse=True) +def expose_internal(controller): + controller.inject_function( + dedent( + """ + @external + def repay_partial( + _for: address, + _debt: uint256, + _wallet_d_debt: uint256, + _approval: bool, + _xy: uint256[2], + _cb: core.IController.CallbackData, + _callbacker: address, + _max_active_band: int256, + _shrink: bool + ) -> uint256: + return core._repay_partial(_for, _debt, _wallet_d_debt, _approval, _xy, _cb, _callbacker, _max_active_band, _shrink) + """ + ) + ) + + +@pytest.fixture(scope="module") +def snapshot(controller, amm, fake_leverage): + def fn(token, borrower, payer): + return { + "controller": token.balanceOf(controller), + "borrower": token.balanceOf(borrower), + "payer": token.balanceOf(payer), + "amm": token.balanceOf(amm), + "callback": token.balanceOf(fake_leverage), + } + + return fn + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_wallet( + controller, borrowed_token, collateral_token, amm, snapshot, admin, different_payer +): + """ + Test partial repayment using only wallet tokens (no soft-liquidation). + + Money Flow: wallet_borrowed (wallet) → Controller + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Set liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + ticks_before = amm.read_user_tick_numbers(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 + + # ================= Setup payer tokens ================= + + wallet_borrowed = debt // 2 # Repay half the debt + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # _shrink == False, works both with and without approval + with boa.env.anchor(): + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # No borrowed tokens in AMM + assert xy_after[1] == xy_before[1] # Collateral still in AMM + + # Check that ticks moved up after partial repayment (position improved) + ticks_after = amm.read_user_tick_numbers(borrower) + assert ticks_after[0] > ticks_before[0] # Lower tick moved up + assert ticks_after[1] > ticks_before[1] # Upper tick moved up + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - wallet_borrowed + assert state_logs[0].collateral == xy_before[1] # Position still has collateral + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == wallet_borrowed + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in partial repay + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment using wallet + callback tokens (not underwater). + + Money Flow: wallet_borrowed (wallet) + cb.borrowed (callback) → Controller + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + ticks_before = amm.read_user_tick_numbers(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 # Position is healthy + + # ================= Setup callback tokens ================= + + callback_borrowed = debt // 2 # Callback provides half of partial debt + callback_collateral = COLLATERAL - 100 # Some collateral from callback + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, COLLATERAL, sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt - 1) + cb = (amm.active_band(), callback_borrowed, callback_collateral) + + # ================= Capture balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # _shrink == False, works both with and without approval + with boa.env.anchor(): + controller.inject.repay_partial( + borrower, + debt, + 0, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + 0, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_from_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # No borrowed tokens in AMM (healthy position) + assert xy_after[1] == callback_collateral # Collateral still in AMM + + # Check that ticks moved up after partial repayment (position improved) + ticks_after = amm.read_user_tick_numbers(borrower) + assert ticks_after[0] > ticks_before[0] # Lower tick moved up + assert ticks_after[1] > ticks_before[1] # Upper tick moved up + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - callback_borrowed + assert ( + state_logs[0].collateral == callback_collateral + ) # Position still has collateral + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == callback_borrowed + assert repay_logs[0].collateral_decrease == xy_before[1] - callback_collateral + + # ================= Verify money flows ================= + + assert borrowed_to_controller == callback_borrowed + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_amm == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_wallet_and_callback( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment using wallet + callback tokens (not underwater). + + Money Flow: wallet_borrowed (wallet) + cb.borrowed (callback) → Controller + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, 10**18, N_BANDS) + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + ticks_before = amm.read_user_tick_numbers(borrower) + xy_before = amm.get_sum_xy(borrower) + assert xy_before[0] == 0 and xy_before[1] > 0 # Position is healthy + + # ================= Setup payer tokens ================= + + wallet_borrowed = debt // 2 # Repay half the debt + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + + # ================= Setup callback tokens ================= + + callback_borrowed = wallet_borrowed // 2 # Callback provides half of partial debt + callback_collateral = COLLATERAL - 100 # Some collateral from callback + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, COLLATERAL, sender=amm.address) + boa.deal(borrowed_token, fake_leverage, debt - 1) + cb = (amm.active_band(), callback_borrowed, callback_collateral) + + # ================= Capture balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # _shrink == False, works both with and without approval + with boa.env.anchor(): + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + False, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_from_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # No borrowed tokens in AMM (healthy position) + assert xy_after[1] == callback_collateral # Collateral still in AMM + + # Check that ticks moved up after partial repayment (position improved) + ticks_after = amm.read_user_tick_numbers(borrower) + assert ticks_after[0] > ticks_before[0] # Lower tick moved up + assert ticks_after[1] > ticks_before[1] # Upper tick moved up + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - wallet_borrowed - callback_borrowed + assert ( + state_logs[0].collateral == callback_collateral + ) # Position still has collateral + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == wallet_borrowed + callback_borrowed + assert repay_logs[0].collateral_decrease == xy_before[1] - callback_collateral + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + callback_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_amm == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_wallet_underwater( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment from wallet when position is underwater (soft-liquidated). + + Money Flow: wallet_borrowed (wallet) → Controller + Position remains underwater (no collateral return) + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + debt = controller.debt(borrower) + ticks_before = amm.read_user_tick_numbers(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Setup payer tokens ================= + + wallet_borrowed = debt // 3 # Repay 1/3 of the debt + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # Can't use callback underwater if _shrink == False + with boa.reverts(): + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + False, + xy_before, + (0, 0, 0), + fake_leverage, + 2**255 - 1, + False, + ) + # _shrink == False, works both with and without approval + with boa.env.anchor(): + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + False, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == xy_before[0] # Still has borrowed tokens in AMM (underwater) + assert xy_after[1] == xy_before[1] # Still has collateral in AMM + + # Check that ticks stay the same after underwater partial repayment (position still underwater) + ticks_after = amm.read_user_tick_numbers(borrower) + assert ticks_after[0] == ticks_before[0] # Lower tick unchanged + assert ticks_after[1] == ticks_before[1] # Upper tick unchanged + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - wallet_borrowed + assert ( + state_logs[0].collateral == xy_before[1] + ) # Collateral amount after partial repay + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == wallet_borrowed + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_xy0_underwater_shrink( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment from wallet when position is underwater (soft-liquidated). + + Money Flow: xy[0] (AMM) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3 ticks in collateral + # 2 tick active + # 1 tick in borrowed + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + amount_out = amm.bands_y(ticks_before[0]) + amm.bands_y(ticks_before[0] + 1) // 2 + amount_out = amount_out // 10 ** (18 - borrowed_token.decimals()) + amount_in = amm.get_dx(0, 1, amount_out) + boa.deal(borrowed_token, trader, amount_in) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange_dy(0, 1, amount_out, amount_in + 1) + assert controller.tokens_to_shrink(borrower) == 0 + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + 1 + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + # _shrink == True, reverts without approval + with boa.reverts(): + controller.inject.repay_partial( + borrower, + debt, + 0, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + 0, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # Spent all the tokens from AMM (not underwater now) + assert xy_after[1] == xy_before[1] # Still has collateral in AMM + + # Check that user has exited form underwater + ticks_after = amm.read_user_tick_numbers(borrower) + assert ( + ticks_after[1] - ticks_after[0] + 1 == 4 + ) # Size has been shrunk from 6 bands to 4 bands + assert ( + ticks_after[0] > active_band_before + ) # Lower tick is higher than active_band before + assert ( + ticks_after[1] >= ticks_before[1] + ) # Upper tick is higher or the same as before + assert amm.active_band() == active_band_before # Active band unchanged + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - xy_before[0] + assert ( + state_logs[0].collateral == xy_before[1] + ) # Collateral amount after partial repay + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == xy_before[0] + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay + + # ================= Verify money flows ================= + + assert borrowed_to_controller == xy_before[0] + assert borrowed_from_amm == -xy_before[0] + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_xy0_and_wallet_underwater_shrink( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment from wallet when position is underwater (soft-liquidated). + + Money Flow: wallet_borrowed (wallet) + xy[0] (AMM) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, tokens_to_shrink) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # _shrink == True, reverts without approval + with boa.reverts(): + controller.inject.repay_partial( + borrower, + debt, + tokens_to_shrink, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + tokens_to_shrink, + True, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # Spent all the tokens from AMM (not underwater now) + assert xy_after[1] == xy_before[1] # Still has collateral in AMM + + # Check that user has exited form underwater + ticks_after = amm.read_user_tick_numbers(borrower) + assert ( + ticks_after[1] - ticks_after[0] + 1 == 5 + ) # Size has been shrunk from 6 bands to 5 bands + assert ( + ticks_after[0] > active_band_before + ) # Lower tick is higher than active_band before + assert ( + ticks_after[1] >= ticks_before[1] + ) # Upper tick is higher or the same as before + assert amm.active_band() == active_band_before # Active band unchanged + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - xy_before[0] - tokens_to_shrink + assert ( + state_logs[0].collateral == xy_before[1] + ) # Collateral amount after partial repay + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == xy_before[0] + tokens_to_shrink + assert ( + repay_logs[0].collateral_decrease == 0 + ) # No collateral decrease in underwater partial repay + + # ================= Verify money flows ================= + + assert borrowed_to_controller == xy_before[0] + tokens_to_shrink + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_payer == -tokens_to_shrink + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_xy0_and_callback_underwater_shrink( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment from callback when position is underwater (soft-liquidated). + + Money Flow: xy[0] (AMM) + cb.borrowed (callback) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Setup callback tokens ================= + + callback_borrowed = tokens_to_shrink # Callback provides tokens_to_shrink + callback_collateral = xy_before[1] - 100 # Some collateral from callback + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, xy_before[1], sender=amm.address) + boa.deal(borrowed_token, fake_leverage, callback_borrowed) + cb = (amm.active_band(), callback_borrowed, callback_collateral) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + # _shrink == True, reverts without approval + with boa.reverts(): + controller.inject.repay_partial( + borrower, + debt, + 0, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + 0, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_from_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # Spent all the tokens from AMM (not underwater now) + assert xy_after[1] == callback_collateral # Still has collateral in AMM + + # Check that user has exited form underwater + ticks_after = amm.read_user_tick_numbers(borrower) + assert ( + ticks_after[1] - ticks_after[0] + 1 == 5 + ) # Size has been shrunk from 6 bands to 5 bands + assert ( + ticks_after[0] > active_band_before + ) # Lower tick is higher than active_band before + assert ( + ticks_after[1] >= ticks_before[1] + ) # Upper tick is higher or the same as before + assert amm.active_band() == active_band_before # Active band unchanged + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert state_logs[0].debt == debt - xy_before[0] - callback_borrowed + assert ( + state_logs[0].collateral == callback_collateral + ) # Collateral amount after partial repay + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert repay_logs[0].loan_decrease == xy_before[0] + callback_borrowed + assert repay_logs[0].collateral_decrease == xy_before[1] - callback_collateral + + # ================= Verify money flows ================= + + assert borrowed_to_controller == xy_before[0] + callback_borrowed + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + + assert collateral_to_amm == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_from_xy0_and_wallet_and_callback_underwater_shrink( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test partial repayment from callback when position is underwater (soft-liquidated). + + Money Flow: xy[0] (AMM) + cb.borrowed (callback) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Set new liquidation discount ================= + + old_liquidation_discount = controller.liquidation_discount() + new_liquidation_discount = old_liquidation_discount - 1 + controller.set_borrowing_discounts( + controller.loan_discount(), new_liquidation_discount, sender=admin + ) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Setup callback tokens ================= + + callback_borrowed = tokens_to_shrink // 2 # Callback provides tokens_to_shrink + callback_collateral = xy_before[1] - 100 # Some collateral from callback + amm.withdraw(borrower, 10**18, sender=controller.address) + collateral_token.transfer(fake_leverage, xy_before[1], sender=amm.address) + boa.deal(borrowed_token, fake_leverage, callback_borrowed) + cb = (amm.active_band(), callback_borrowed, callback_collateral) + + # ================= Setup payer tokens ================= + + wallet_borrowed = tokens_to_shrink - callback_borrowed + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + with boa.env.prank(payer): + max_approve(borrowed_token, controller) + # _shrink == True, reverts without approval + with boa.reverts(): + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + False, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + assert ( + controller.liquidation_discounts(borrower) == old_liquidation_discount + ) + controller.inject.repay_partial( + borrower, + debt, + wallet_borrowed, + True, + xy_before, + cb, + fake_leverage.address, + 2**255 - 1, + True, + ) + + # ================= Capture logs ================= + + repay_logs = filter_logs(controller, "Repay") + state_logs = filter_logs(controller, "UserState") + assert controller.liquidation_discounts(borrower) == new_liquidation_discount + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_to_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_from_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + xy_after = amm.get_sum_xy(borrower) + assert xy_after[0] == 0 # Spent all the tokens from AMM (not underwater now) + assert xy_after[1] == callback_collateral # Still has collateral in AMM + + # Check that user has exited form underwater + ticks_after = amm.read_user_tick_numbers(borrower) + assert ( + ticks_after[1] - ticks_after[0] + 1 == 5 + ) # Size has been shrunk from 6 bands to 5 bands + assert ( + ticks_after[0] > active_band_before + ) # Lower tick is higher than active_band before + assert ( + ticks_after[1] >= ticks_before[1] + ) # Upper tick is higher or the same as before + assert amm.active_band() == active_band_before # Active band unchanged + + # ================= Verify logs ================= + + assert len(state_logs) == 1 + assert state_logs[0].user == borrower + assert ( + state_logs[0].debt == debt - xy_before[0] - callback_borrowed - wallet_borrowed + ) + assert ( + state_logs[0].collateral == callback_collateral + ) # Collateral amount after partial repay + + assert len(repay_logs) == 1 + assert repay_logs[0].user == borrower + assert ( + repay_logs[0].loan_decrease + == xy_before[0] + callback_borrowed + wallet_borrowed + ) + assert repay_logs[0].collateral_decrease == xy_before[1] - callback_collateral + + # ================= Verify money flows ================= + + assert borrowed_to_controller == xy_before[0] + callback_borrowed + wallet_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_from_amm == -xy_before[0] + assert borrowed_from_callback == -callback_borrowed + + assert collateral_to_amm == callback_collateral + assert collateral_from_callback == -callback_collateral + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_repay_partial_cannot_shrink( + controller, + borrowed_token, + collateral_token, + amm, + snapshot, + admin, + fake_leverage, + different_payer, +): + """ + Test that attempt to shrink the position to less than 4 bands reverts + """ + borrower = payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Create loan ================= + + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + assert debt > 0 + controller.create_loan(COLLATERAL, debt, N_BANDS) + + # ================= Push position to underwater ================= + + # 6, 5, 4 ticks in collateral + # 3 tick active + # 2, 1 tick in borrowed + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + amount_out = ( + amm.bands_y(ticks_before[0]) + + amm.bands_y(ticks_before[0] + 1) + + amm.bands_y(ticks_before[0] + 2) // 2 + ) + amount_out = amount_out // 10 ** (18 - borrowed_token.decimals()) + amount_in = amm.get_dx(0, 1, amount_out) + boa.deal(borrowed_token, trader, amount_in) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange_dy(0, 1, amount_out, amount_in + 1) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + 2 + debt = controller.debt(borrower) + xy_before = amm.get_sum_xy(borrower) + assert 0 < xy_before[0] < debt and xy_before[1] > 0 # Position is underwater + + # ================= Execute partial repayment ================= + + with boa.reverts("Can't shrink"): + controller.tokens_to_shrink(borrower) + + with boa.env.prank(payer): + with boa.reverts("Can't shrink"): + controller.inject.repay_partial( + borrower, + debt, + 0, + False, + xy_before, + (0, 0, 0), + ZERO_ADDRESS, + 2**255 - 1, + True, + ) diff --git a/tests/unitary/controller/test_repay.py b/tests/unitary/controller/test_repay.py new file mode 100644 index 00000000..376da3d0 --- /dev/null +++ b/tests/unitary/controller/test_repay.py @@ -0,0 +1,1813 @@ +import pytest +import boa +from eth_abi import encode + +from tests.utils import max_approve +from tests.utils.constants import MAX_UINT256, ZERO_ADDRESS + +COLLATERAL = 10**17 +N_BANDS = 6 + + +@pytest.fixture(scope="module") +def create_loan(controller, collateral_token, borrowed_token): + def fn(max_debt=False): + borrower = boa.env.eoa + boa.deal(collateral_token, borrower, COLLATERAL) + max_approve(collateral_token, controller) + max_approve(borrowed_token, controller) + debt = 10**18 + if max_debt: + debt = controller.max_borrowable(COLLATERAL, N_BANDS) + controller.create_loan(COLLATERAL, debt, N_BANDS) + assert debt == controller.debt(borrower) + return borrower + + return fn + + +@pytest.fixture(scope="module") +def get_calldata(dummy_callback, borrowed_token, collateral_token): + def fn(borrowed_amount, collateral_amount): + return encode( + ["address", "address", "uint256", "uint256"], + [ + borrowed_token.address, + collateral_token.address, + borrowed_amount, + collateral_amount, + ], + ) + + return fn + + +@pytest.fixture(scope="module") +def snapshot(controller, amm, dummy_callback): + def fn(token, borrower, payer): + return { + "borrower": token.balanceOf(borrower), + "payer": token.balanceOf(payer), + "controller": token.balanceOf(controller), + "amm": token.balanceOf(amm), + "callback": token.balanceOf(dummy_callback), + } + + return fn + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_wallet( + controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer +): + """ + Test full repayment using wallet tokens. + + Money Flow: debt (payer) → Controller + COLLATERAL (AMM) → Borrower + Position is fully closed + """ + borrower = create_loan() + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, debt) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + controller.repay(MAX_UINT256, borrower, sender=payer) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_payer == -debt + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == COLLATERAL + assert collateral_from_amm == -COLLATERAL + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test full repayment using callback tokens. + + Money Flow: callback_borrowed (callback) → Controller + callback_borrowed - debt (callback) → Borrower (excess) + COLLATERAL (AMM) → Callback → Borrower (excess) + Position is fully closed + """ + borrower = create_loan() + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + callback_borrowed = debt + 1 + callback_collateral = COLLATERAL // 2 + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_to_borrower == 1 + assert borrowed_from_callback == -(debt + 1) + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_borrower == COLLATERAL // 2 + assert collateral_to_callback == COLLATERAL // 2 + assert collateral_from_amm == -COLLATERAL + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_xy0( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test full repayment when position is in soft-liquidation (underwater). + + Money Flow: xy[0] (AMM) → Controller + xy[0] - debt (Controller) → Borrower (excess) + xy[1] (AMM) → Borrower + Position is fully closed + """ + borrower = create_loan(max_debt=True) + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + debt = controller.debt(borrower) + assert debt > 0 + boa.deal(borrowed_token, trader, debt * 10_001 // 10_000) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt * 10_001 // 10_000, 0) + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert user_state_before[1] > debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with xy0 reverts without approval + with boa.reverts(): + controller.repay(MAX_UINT256, borrower, sender=payer) + controller.approve(payer, True, sender=borrower) + controller.repay(MAX_UINT256, borrower, sender=payer) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_to_borrower == user_state_before[1] - debt + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == user_state_before[0] + assert collateral_from_amm == -user_state_before[0] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_wallet_and_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test full repayment using both wallet and callback tokens. + + Money Flow: callback_borrowed (callback) + (debt - callback_borrowed) (wallet) → Controller + COLLATERAL (AMM) → Callback → Borrower (excess) + Position is fully closed + """ + borrower = create_loan() + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + callback_borrowed = debt - 1 + callback_collateral = COLLATERAL // 2 + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, 1) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + MAX_UINT256, + borrower, + amm.active_band(), + dummy_callback, + calldata, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + MAX_UINT256, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_callback == -callback_borrowed + assert borrowed_from_payer == -1 + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_borrower == COLLATERAL - callback_collateral + assert collateral_to_callback == callback_collateral + assert collateral_from_amm == -COLLATERAL + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_xy0_and_wallet( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test full repayment when position is underwater using AMM + wallet tokens. + + Money Flow: xy[0] (AMM) + (debt - xy[0]) (wallet) → Controller + xy[1] (AMM) → Borrower + Position is fully closed + """ + borrower = create_loan(max_debt=True) + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + debt = controller.debt(borrower) + assert debt > 0 + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, debt - user_state_before[1]) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with xy0 reverts without approval + with boa.reverts(): + controller.repay(MAX_UINT256, borrower, sender=payer) + controller.approve(payer, True, sender=borrower) + controller.repay(MAX_UINT256, borrower, sender=payer) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_payer == -(debt - user_state_before[1]) + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_to_borrower == user_state_before[0] + assert collateral_from_amm == -user_state_before[0] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_xy0_and_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test full repayment when position is underwater using AMM + callback tokens. + + Money Flow: xy[0] (AMM) + callback_borrowed (callback) → Controller + xy[0] + callback_borrowed - debt (AMM) → Borrower (excess) + xy[1] (AMM) → Callback → Borrower (excess) + Position is fully closed + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + debt = controller.debt(borrower) + assert debt > 0 + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + callback_borrowed = debt - user_state_before[1] + 1 + callback_collateral = user_state_before[0] // 3 + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_to_borrower = ( + borrowed_token_after["borrower"] - borrowed_token_before["borrower"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_callback == -callback_borrowed + assert borrowed_to_borrower == 1 + + assert collateral_to_borrower == callback_collateral + assert collateral_to_callback == user_state_before[0] - callback_collateral + assert collateral_from_amm == -user_state_before[0] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_full_repay_from_xy0_and_wallet_and_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test full repayment when position is underwater using all three sources: AMM + wallet + callback. + + Money Flow: xy[0] (AMM) + callback_borrowed (callback) + (debt - xy[0] - callback_borrowed) (wallet) → Controller + xy[1] (AMM) → Callback → Borrower (excess) + Position is fully closed + """ + borrower = create_loan(max_debt=True) + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to soft-liquidation ================= + + trader = boa.env.generate_address() + debt = controller.debt(borrower) + assert debt > 0 + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + callback_borrowed = debt - user_state_before[1] - 1 + callback_collateral = user_state_before[0] // 3 + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, debt - user_state_before[1] - callback_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute full repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + MAX_UINT256, + borrower, + amm.active_band(), + dummy_callback, + calldata, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + MAX_UINT256, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_borrower = ( + collateral_token_after["borrower"] - collateral_token_before["borrower"] + ) + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == 0 # collateral in AMM + assert user_state_after[1] == 0 # borrowed in AMM + assert user_state_after[2] == 0 # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - debt + assert controller.eval("core.repaid") == repaid + debt + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == debt + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_callback == -callback_borrowed + assert borrowed_from_payer == -1 + + assert collateral_to_borrower == callback_collateral + assert collateral_to_callback == user_state_before[0] - callback_collateral + assert collateral_from_amm == -user_state_before[0] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_wallet( + controller, borrowed_token, collateral_token, create_loan, snapshot, different_payer +): + """ + Test partial repayment using wallet tokens. + + Money Flow: wallet_borrowed (payer) → Controller + Position remains active with reduced debt + """ + borrower = create_loan() + + payer = boa.env.eoa + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + wallet_borrowed = debt // 2 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + controller.repay(wallet_borrowed, borrower, sender=payer) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == user_state_before[0] # collateral in AMM + assert user_state_after[1] == user_state_before[1] # borrowed in AMM + assert user_state_after[2] == debt - wallet_borrowed # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - wallet_borrowed + assert controller.eval("core.repaid") == repaid + wallet_borrowed + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test partial repayment using callback tokens. + + Money Flow: callback_borrowed (callback) → Controller + xy[0] (AMM) -> Callback callback_collateral → AMM (remaining) + Position remains active with reduced debt + """ + borrower = create_loan() + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + callback_borrowed = debt // 3 + callback_collateral = COLLATERAL * 3 // 4 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == callback_collateral # collateral in AMM + assert user_state_after[1] == user_state_before[1] # borrowed in AMM + assert user_state_after[2] == debt - callback_borrowed # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - callback_borrowed + assert controller.eval("core.repaid") == repaid + callback_borrowed + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == callback_borrowed + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + + assert collateral_to_callback == COLLATERAL - callback_collateral + assert collateral_from_amm == -(COLLATERAL - callback_collateral) + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_wallet_and_callback( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test partial repayment using both wallet and callback tokens. + + Money Flow: wallet_borrowed (wallet) + callback_borrowed (callback) → Controller + callback_collateral (callback) → AMM (remaining) + Position remains active with reduced debt + """ + borrower = create_loan() + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + wallet_borrowed = debt // 3 # Wallet provides 1/3 of debt + callback_borrowed = debt // 3 # Callback provides 1/3 of debt + callback_collateral = COLLATERAL * 2 // 3 # Callback provides 2/3 of collateral + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Setup callback tokens ================= + + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay with callback reverts without approval + with boa.reverts(): + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + sender=payer, + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == callback_collateral # collateral in AMM + assert user_state_after[1] == user_state_before[1] # borrowed in AMM + assert user_state_after[2] == debt - wallet_borrowed - callback_borrowed # debt + assert user_state_after[3] == user_state_before[3] # N + assert controller.total_debt() == total_debt - wallet_borrowed - callback_borrowed + assert ( + controller.eval("core.repaid") == repaid + wallet_borrowed + callback_borrowed + ) + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + callback_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + + assert collateral_to_callback == COLLATERAL - callback_collateral + assert collateral_from_amm == -(COLLATERAL - callback_collateral) + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_wallet_underwater( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test partial repayment from wallet when position is underwater (soft-liquidated). + + Money Flow: wallet_borrowed (wallet) → Controller + Position remains underwater (no collateral return) + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert debt > 0 + trader = boa.env.generate_address() + boa.deal(borrowed_token, trader, debt // 2) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, debt // 2, 0) + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + wallet_borrowed = debt // 3 # Repay 1/3 of the debt + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + controller.repay(wallet_borrowed, borrower, amm.active_band(), sender=payer) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == user_state_before[0] # collateral in AMM unchanged + assert user_state_after[1] == user_state_before[1] # borrowed in AMM unchanged + assert user_state_after[2] == debt - wallet_borrowed # debt reduced + assert user_state_after[3] == user_state_before[3] # N unchanged + assert controller.total_debt() == total_debt - wallet_borrowed + assert controller.eval("core.repaid") == repaid + wallet_borrowed + + # ================= Verify money flows ================= + + assert borrowed_to_controller == wallet_borrowed + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_token_after["amm"] == borrowed_token_before["amm"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_xy0_underwater_shrink( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test partial repayment from xy[0] when position is underwater (soft-liquidated) with shrink. + + Money Flow: xy[0] (AMM) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3 ticks in collateral + # 2 tick active + # 1 tick in borrowed + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + amount_out = amm.bands_y(ticks_before[0]) + amm.bands_y(ticks_before[0] + 1) // 2 + amount_out = amount_out // 10 ** (18 - borrowed_token.decimals()) + amount_in = amm.get_dx(0, 1, amount_out) + boa.deal(borrowed_token, trader, amount_in) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange_dy(0, 1, amount_out, amount_in + 1) + assert controller.tokens_to_shrink(borrower) == 0 + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay reverts without approval + with boa.reverts(): + controller.repay( + 0, borrower, amm.active_band(), ZERO_ADDRESS, b"", True, sender=payer + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + 0, borrower, amm.active_band(), ZERO_ADDRESS, b"", True, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == user_state_before[0] # collateral in AMM unchanged + assert user_state_after[1] == 0 # no borrowed tokens in AMM (exited underwater) + assert user_state_after[2] == debt - user_state_before[1] # debt reduced by xy[0] + assert user_state_after[3] == user_state_before[3] - 2 # N shrunk by 2 + assert controller.total_debt() == total_debt - user_state_before[1] + assert controller.eval("core.repaid") == repaid + user_state_before[1] + + # ================= Verify money flows ================= + + assert borrowed_to_controller == user_state_before[1] + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_xy0_and_wallet_underwater_shrink( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test partial repayment from xy[0] + wallet when position is underwater (soft-liquidated) with shrink. + + Money Flow: xy[0] (AMM) + wallet_borrowed (wallet) → Controller + Position exits from underwater (no collateral return though) + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + wallet_borrowed = tokens_to_shrink # Additional wallet repayment + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay reverts without approval + with boa.reverts(): + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + ZERO_ADDRESS, + b"", + True, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + ZERO_ADDRESS, + b"", + True, + sender=payer, + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == user_state_before[0] # collateral in AMM unchanged + assert user_state_after[1] == 0 # no borrowed tokens in AMM (exited underwater) + assert ( + user_state_after[2] == debt - user_state_before[1] - wallet_borrowed + ) # debt reduced + assert user_state_after[3] == user_state_before[3] - 1 # N shrunk by 1 + assert ( + controller.total_debt() == total_debt - user_state_before[1] - wallet_borrowed + ) + assert ( + controller.eval("core.repaid") + == repaid + user_state_before[1] + wallet_borrowed + ) + + # ================= Verify money flows ================= + + assert borrowed_to_controller == user_state_before[1] + wallet_borrowed + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_token_after["callback"] == borrowed_token_before["callback"] + + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + assert collateral_token_after["amm"] == collateral_token_before["amm"] + assert collateral_token_after["callback"] == collateral_token_before["callback"] + assert collateral_token_after["controller"] == collateral_token_before["controller"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_xy0_and_callback_underwater_shrink( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test partial repayment from xy[0] + callback when position is underwater (soft-liquidated) with shrink. + + Money Flow: xy[0] (AMM) + callback_borrowed (callback) → Controller + callback_collateral (callback) → AMM (remaining) + Position exits from underwater + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup callback tokens ================= + + callback_borrowed = tokens_to_shrink # Additional callback repayment + callback_collateral = user_state_before[0] - 100 # Some collateral from callback + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay reverts without approval + with boa.reverts(): + controller.repay( + 0, + borrower, + amm.active_band(), + dummy_callback, + calldata, + True, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + 0, borrower, amm.active_band(), dummy_callback, calldata, True, sender=payer + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == callback_collateral # collateral in AMM + assert user_state_after[1] == 0 # no borrowed tokens in AMM (exited underwater) + assert ( + user_state_after[2] == debt - user_state_before[1] - callback_borrowed + ) # debt reduced + assert user_state_after[3] == user_state_before[3] - 1 # N shrunk by 1 + assert ( + controller.total_debt() == total_debt - user_state_before[1] - callback_borrowed + ) + assert ( + controller.eval("core.repaid") + == repaid + user_state_before[1] + callback_borrowed + ) + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert borrowed_to_controller == user_state_before[1] + callback_borrowed + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_callback == -callback_borrowed + assert borrowed_token_after["payer"] == borrowed_token_before["payer"] + + assert collateral_to_callback == user_state_before[0] - callback_collateral + assert collateral_from_amm == -(user_state_before[0] - callback_collateral) + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_from_xy0_and_wallet_and_callback_underwater_shrink( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + dummy_callback, + get_calldata, + snapshot, + different_payer, +): + """ + Test partial repayment from xy[0] + wallet + callback when position is underwater (soft-liquidated) with shrink. + + Money Flow: xy[0] (AMM) + wallet_borrowed (wallet) + callback_borrowed (callback) → Controller + callback_collateral (callback) → AMM (remaining) + Position exits from underwater + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + # 6, 5, 4, 3, 2 ticks in collateral + # 1 tick active + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + boa.deal(borrowed_token, trader, 10 ** (borrowed_token.decimals() // 2)) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange(0, 1, 10 ** (borrowed_token.decimals() // 2), 0) + assert controller.user_state(borrower)[1] > 0 + tokens_to_shrink = controller.tokens_to_shrink(borrower) + assert tokens_to_shrink > 0 + + # ================= Capture initial state ================= + + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert 0 < user_state_before[1] < debt and user_state_before[0] > 0 + total_debt = controller.total_debt() + repaid = controller.eval("core.repaid") + + # ================= Setup payer tokens ================= + + wallet_borrowed = tokens_to_shrink // 3 # Wallet provides 1/6 of debt + if different_payer: + boa.deal(borrowed_token, payer, wallet_borrowed) + max_approve(borrowed_token, controller, sender=payer) + + # ================= Setup callback tokens ================= + + callback_borrowed = ( + tokens_to_shrink - wallet_borrowed + ) # Callback provides 1/6 of debt + callback_collateral = user_state_before[0] - 100 # Some collateral from callback + calldata = get_calldata(callback_borrowed, callback_collateral) + boa.deal(borrowed_token, dummy_callback, callback_borrowed) + repay_hits = dummy_callback.callback_repay_hits() + + # ================= Capture initial balances ================= + + borrowed_token_before = snapshot(borrowed_token, borrower, payer) + collateral_token_before = snapshot(collateral_token, borrower, payer) + + # ================= Execute partial repayment ================= + + if different_payer: + # Repay reverts without approval + with boa.reverts(): + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + True, + sender=payer, + ) + controller.approve(payer, True, sender=borrower) + controller.repay( + wallet_borrowed, + borrower, + amm.active_band(), + dummy_callback, + calldata, + True, + sender=payer, + ) + + # ================= Capture final balances ================= + + borrowed_token_after = snapshot(borrowed_token, borrower, payer) + collateral_token_after = snapshot(collateral_token, borrower, payer) + + # ================= Calculate money flows ================= + + borrowed_to_controller = ( + borrowed_token_after["controller"] - borrowed_token_before["controller"] + ) + borrowed_from_payer = borrowed_token_after["payer"] - borrowed_token_before["payer"] + borrowed_from_amm = borrowed_token_after["amm"] - borrowed_token_before["amm"] + borrowed_from_callback = ( + borrowed_token_after["callback"] - borrowed_token_before["callback"] + ) + collateral_from_amm = collateral_token_after["amm"] - collateral_token_before["amm"] + collateral_to_callback = ( + collateral_token_after["callback"] - collateral_token_before["callback"] + ) + + # ================= Verify position state ================= + + user_state_after = controller.user_state(borrower) + assert user_state_after[0] == callback_collateral # collateral in AMM + assert user_state_after[1] == 0 # no borrowed tokens in AMM (exited underwater) + assert ( + user_state_after[2] + == debt - user_state_before[1] - wallet_borrowed - callback_borrowed + ) # debt reduced + assert user_state_after[3] == user_state_before[3] - 1 # N shrunk by 1 + assert ( + controller.total_debt() + == total_debt - user_state_before[1] - wallet_borrowed - callback_borrowed + ) + assert ( + controller.eval("core.repaid") + == repaid + user_state_before[1] + wallet_borrowed + callback_borrowed + ) + assert dummy_callback.callback_repay_hits() == repay_hits + 1 + + # ================= Verify money flows ================= + + assert ( + borrowed_to_controller + == user_state_before[1] + wallet_borrowed + callback_borrowed + ) + assert borrowed_from_payer == -wallet_borrowed + assert borrowed_from_amm == -user_state_before[1] + assert borrowed_from_callback == -callback_borrowed + + assert collateral_to_callback == user_state_before[0] - callback_collateral + assert collateral_from_amm == -(user_state_before[0] - callback_collateral) + assert collateral_token_after["controller"] == collateral_token_before["controller"] + assert collateral_token_after["borrower"] == collateral_token_before["borrower"] + + if different_payer: + assert borrowed_token_after["borrower"] == borrowed_token_before["borrower"] + assert collateral_token_after["payer"] == collateral_token_before["payer"] + + +@pytest.mark.parametrize("different_payer", [True, False]) +def test_partial_repay_cannot_shrink( + controller, + amm, + borrowed_token, + collateral_token, + create_loan, + snapshot, + different_payer, +): + """ + Test that attempt to shrink the position to less than 4 bands reverts with "Can't shrink" error. + """ + borrower = create_loan(max_debt=True) + + payer = borrower + if different_payer: + payer = boa.env.generate_address() + + # ================= Push position to underwater ================= + + # 6, 5, 4 ticks in collateral + # 3 tick active + # 2, 1 tick in borrowed + trader = boa.env.generate_address() + ticks_before = amm.read_user_tick_numbers(borrower) + assert ticks_before[1] - ticks_before[0] == 5 + amount_out = ( + amm.bands_y(ticks_before[0]) + + amm.bands_y(ticks_before[0] + 1) + + amm.bands_y(ticks_before[0] + 2) // 2 + ) + amount_out = amount_out // 10 ** (18 - borrowed_token.decimals()) + amount_in = amm.get_dx(0, 1, amount_out) + boa.deal(borrowed_token, trader, amount_in) + with boa.env.prank(trader): + max_approve(borrowed_token, amm) + amm.exchange_dy(0, 1, amount_out, amount_in + 1) + + # ================= Capture initial state ================= + + active_band_before = amm.active_band() + assert active_band_before == ticks_before[0] + 2 + user_state_before = controller.user_state(borrower) + debt = user_state_before[2] + assert ( + 0 < user_state_before[1] < debt and user_state_before[0] > 0 + ) # Position is underwater + + # ================= Verify shrink reverts ================= + + with boa.reverts("Can't shrink"): + controller.tokens_to_shrink(borrower) + + with boa.reverts("Can't shrink"): + controller.repay( + 0, borrower, amm.active_band(), ZERO_ADDRESS, b"", True, sender=payer + ) diff --git a/tests/unitary/lending/conftest.py b/tests/unitary/lending/conftest.py new file mode 100644 index 00000000..fbf7c522 --- /dev/null +++ b/tests/unitary/lending/conftest.py @@ -0,0 +1,33 @@ +import pytest +import boa + + +@pytest.fixture(scope="module") +def market_type(): + return "lending" + + +@pytest.fixture(scope="module") +def make_debt(vault, controller, amm, borrowed_token): + borrowed_balance = controller.available_balance() + debt = borrowed_balance // 2 + assert debt > 0 + rate_mul = int(1.2 * 10**18) + _total_debt_rate_mul = int(1.1 * 10**18) + amm.eval(f"self.rate_mul = {rate_mul}") + controller.eval(f"core._total_debt.initial_debt = {debt}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + +@pytest.fixture(scope="module") +def deposit_into_vault(vault, controller, amm, borrowed_token): + def f(user=boa.env.eoa, assets=100 * 10 ** borrowed_token.decimals()): + assert assets > 0 + boa.deal(borrowed_token, user, assets) + initial_balance = controller.available_balance() + with boa.env.prank(user): + borrowed_token.approve(vault, assets) + vault.deposit(assets) + assert controller.available_balance() == initial_balance + assets + + return f diff --git a/tests/unitary/lending/lend_controller/test_admin_percentage_lc.py b/tests/unitary/lending/lend_controller/test_admin_percentage_lc.py new file mode 100644 index 00000000..b2e0a337 --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_admin_percentage_lc.py @@ -0,0 +1,38 @@ +import boa + +from tests.utils import max_approve +from tests.utils.constants import MIN_TICKS, WAD + +COLLATERAL = 10**21 +DEBT = 10**18 +RATE = 10**11 +TIME_DELTA = 86400 + + +def test_default_behavior_no_fees(controller): + assert controller.admin_percentage() == 0 + + +def test_default_behavior_with_interest( + admin, + controller, + amm, + collateral_token, +): + def outstanding_for_pct(pct: int) -> int: + with boa.env.anchor(): + controller.set_admin_percentage(WAD * pct // 100, sender=admin) + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) + + return controller.admin_fees() + + for pct in range(1, 101): + assert ( + outstanding_for_pct(pct) == DEBT * TIME_DELTA * RATE // 10**18 * pct // 100 + ) diff --git a/tests/unitary/lending/lend_controller/test_borrow_more_lc.py b/tests/unitary/lending/lend_controller/test_borrow_more_lc.py new file mode 100644 index 00000000..2933d49a --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_borrow_more_lc.py @@ -0,0 +1,23 @@ +import boa + +from tests.utils import max_approve + + +def test_default_behavior(controller, collateral_token): + COLLATERAL = 10**23 + INITIAL_DEBT = 10**20 + EXTRA_COLLATERAL = 10**22 + EXTRA_DEBT = 10**19 + BANDS = 5 + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL + EXTRA_COLLATERAL) + max_approve(collateral_token, controller.address) + + assert controller.lent() == 0 + + controller.create_loan(COLLATERAL, INITIAL_DEBT, BANDS) + assert controller.lent() == INITIAL_DEBT + + controller.borrow_more(EXTRA_COLLATERAL, EXTRA_DEBT) + + assert controller.lent() == INITIAL_DEBT + EXTRA_DEBT diff --git a/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py new file mode 100644 index 00000000..ef3899c0 --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_borrowed_balance_lc.py @@ -0,0 +1,133 @@ +import boa + +from tests.utils import max_approve +from tests.utils.constants import MIN_TICKS, WAD + +COLLATERAL = 10**21 +DEPOSIT = 10**18 +DEBT = 10**18 + + +def snapshot(controller, vault): + return { + "borrowed": controller.available_balance(), + "lent": controller.lent(), + "repaid": controller.repaid(), + "collected": controller.collected(), + "processed": controller.processed(), + "asset_balance": vault.net_deposits(), + } + + +def expect_same(before, after, *fields): + for field in fields: + assert after[field] == before[field] + + +def test_increases_after_deposit(controller, vault, borrowed_token): + boa.deal(borrowed_token, boa.env.eoa, DEPOSIT) + max_approve(borrowed_token, vault.address) + + before = snapshot(controller, vault) + vault.deposit(DEPOSIT) + after = snapshot(controller, vault) + + assert after["borrowed"] == before["borrowed"] + DEPOSIT + assert after["asset_balance"] == before["asset_balance"] + DEBT + expect_same(before, after, "lent", "repaid", "collected", "processed") + + +def test_decreases_after_withdraw( + controller, + vault, + collateral_token, + borrowed_token, +): + boa.deal(borrowed_token, boa.env.eoa, DEPOSIT) + max_approve(borrowed_token, vault.address) + vault.deposit(DEPOSIT) + + before = snapshot(controller, vault) + vault.withdraw(DEPOSIT) + after = snapshot(controller, vault) + + assert after["borrowed"] == before["borrowed"] - DEPOSIT + assert after["asset_balance"] == before["asset_balance"] - DEPOSIT + expect_same(before, after, "lent", "repaid", "collected", "processed") + + +def test_decreases_after_create(controller, vault, collateral_token): + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address) + + before = snapshot(controller, vault) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + after = snapshot(controller, vault) + + assert after["borrowed"] == before["borrowed"] - DEBT + assert after["lent"] == before["lent"] + DEBT + assert after["processed"] == before["processed"] + DEBT + expect_same(before, after, "repaid", "collected", "asset_balance") + + +def test_decreases_after_borrow_more(controller, vault, collateral_token): + boa.deal(collateral_token, boa.env.eoa, 2 * COLLATERAL) + max_approve(collateral_token, controller.address) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + before = snapshot(controller, vault) + controller.borrow_more(COLLATERAL, DEBT) + after = snapshot(controller, vault) + + assert after["borrowed"] == before["borrowed"] - DEBT + assert after["lent"] == before["lent"] + DEBT + assert after["processed"] == before["processed"] + DEBT + expect_same(before, after, "repaid", "collected", "asset_balance") + + +def test_increases_after_repay(controller, vault, collateral_token, borrowed_token): + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address) + max_approve(borrowed_token, controller.address) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + before = snapshot(controller, vault) + controller.repay(DEBT) + after = snapshot(controller, vault) + + assert after["borrowed"] == before["borrowed"] + DEBT + assert after["repaid"] == before["repaid"] + DEBT + expect_same(before, after, "lent", "collected", "asset_balance", "processed") + + +def test_collect_fees_reduces_balance( + admin, + controller, + amm, + collateral_token, + borrowed_token, + vault, +): + ADMIN_FEE = WAD // 2 + RATE = 10**11 + TIME_DELTA = 86400 + + controller.set_admin_fee(ADMIN_FEE, sender=admin) + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) + + before = snapshot(controller, vault) + amount = controller.collect_fees() + after = snapshot(controller, vault) + + assert amount > 0 + assert after["collected"] == before["collected"] + amount + assert after["borrowed"] == before["borrowed"] - amount + assert after["processed"] == after["repaid"] + controller.total_debt() + expect_same(before, after, "lent", "repaid", "asset_balance") diff --git a/tests/unitary/lending/lend_controller/test_collect_fees_lc.py b/tests/unitary/lending/lend_controller/test_collect_fees_lc.py new file mode 100644 index 00000000..21e399ff --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_collect_fees_lc.py @@ -0,0 +1,72 @@ +import boa + +from tests.utils import max_approve +from tests.utils.constants import MIN_TICKS, WAD + +COLLATERAL = 10**21 +DEBT = 10**18 +RATE = 10**11 +TIME_DELTA = 86400 + + +def test_default_behavior_no_fees(controller): + assert controller.collected() == 0 + amount = controller.collect_fees() + assert amount == 0 + assert controller.collected() == 0 + + +def test_collect_fees_accrues_interest( + admin, + controller, + amm, + collateral_token, + borrowed_token, +): + def collect_for_pct(pct: int) -> int: + with boa.env.anchor(): + controller.set_admin_percentage(WAD * pct // 100, sender=admin) + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller) + controller.create_loan(COLLATERAL, DEBT, MIN_TICKS) + + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) + + expected = controller.admin_fees() + amount = controller.collect_fees() + assert controller.collected() == amount == expected + + return amount + + for pct in range(1, 101): + assert collect_for_pct(pct) == DEBT * TIME_DELTA * RATE // 10**18 * pct // 100 + + +def test_collect_fees_reverts_if_not_enough_balance( + admin, + controller, + amm, + collateral_token, + borrowed_token, +): + controller.set_admin_percentage(WAD, sender=admin) + debt = controller.available_balance() + collateral = 10 * debt * amm.price_oracle() // 10**18 + boa.deal(collateral_token, boa.env.eoa, collateral) + max_approve(collateral_token, controller) + max_approve(borrowed_token, controller) + controller.create_loan(collateral, debt, MIN_TICKS) + + amm.eval(f"self.rate = {RATE}") + amm.eval("self.rate_time = block.timestamp") + boa.env.time_travel(TIME_DELTA) + + expected = controller.admin_fees() + assert expected > 0 + with boa.reverts(): + controller.collect_fees() + controller.repay(expected) + amount = controller.collect_fees() + assert controller.collected() == amount == expected diff --git a/tests/unitary/lending/lend_controller/test_create_loan_lc.py b/tests/unitary/lending/lend_controller/test_create_loan_lc.py new file mode 100644 index 00000000..27f0a58b --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_create_loan_lc.py @@ -0,0 +1,18 @@ +import boa + +from tests.utils import max_approve + + +def test_default_behavior(controller, collateral_token): + COLLATERAL = 10**23 + DEBT = 10**20 + BANDS = 5 + + boa.deal(collateral_token, boa.env.eoa, COLLATERAL) + max_approve(collateral_token, controller.address) + + assert controller.lent() == 0 + + controller.create_loan(COLLATERAL, DEBT, BANDS) + + assert controller.lent() == DEBT diff --git a/tests/unitary/lending/lend_controller/test_ctor_lc.py b/tests/unitary/lending/lend_controller/test_ctor_lc.py new file mode 100644 index 00000000..8d5daf6d --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_ctor_lc.py @@ -0,0 +1,44 @@ +import pytest +from tests.utils.constants import MAX_UINT256 + + +@pytest.fixture(scope="module") +def fresh_market( + proto, + borrowed_token, + collateral_token, + amm_A, + amm_fee, + loan_discount, + liquidation_discount, + price_oracle, + min_borrow_rate, + max_borrow_rate, +): + return proto.create_lending_market( + borrowed_token=proto.crvUSD, + collateral_token=collateral_token, + A=amm_A, + fee=amm_fee, + loan_discount=loan_discount, + liquidation_discount=liquidation_discount, + price_oracle=price_oracle, + name="Borrow Cap", + min_borrow_rate=min_borrow_rate, + max_borrow_rate=max_borrow_rate, + seed_amount=0, + ) + + +def test_default_behavior(fresh_market, borrowed_token, proto): + """ + Checks that freshly deployed lending controllers have a borrow cap of zero, + right vault address and infinite allowance to the vault. + """ + controller = fresh_market["controller"] + vault = fresh_market["vault"] + + assert controller.vault() == vault.address + assert controller.borrow_cap() == 0 + approved = borrowed_token.allowance(controller, vault) + assert approved == MAX_UINT256 diff --git a/tests/unitary/lending/lend_controller/test_on_debt_increase.py b/tests/unitary/lending/lend_controller/test_on_debt_increase.py new file mode 100644 index 00000000..efa6f24f --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_on_debt_increase.py @@ -0,0 +1,31 @@ +import boa +import pytest + +BORROW_CAP = 5 * 10**18 + + +@pytest.fixture(scope="module") +def borrow_cap(): + return BORROW_CAP + + +def test_default_behavior(controller, borrow_cap): + """Test that _on_debt_increased works when called by the controller itself with valid debt amount.""" + debt_amount = BORROW_CAP // 2 + controller._on_debt_increased(debt_amount, sender=controller.address) + + +def test_unauthorized(controller, alice): + """Test that _on_debt_increased reverts when called by unauthorized address.""" + debt_amount = 100 * 10**18 + + # Should revert when called by non-controller address + with boa.reverts(dev="virtual method protection"): + controller._on_debt_increased(debt_amount, sender=alice) + + +def test_exceed_borrow_cap(controller, borrow_cap): + """Test that _on_debt_increased reverts when debt exceeds borrow cap.""" + excessive_debt = borrow_cap + 1 + with boa.reverts("Borrow cap exceeded"): + controller._on_debt_increased(excessive_debt, sender=controller.address) diff --git a/tests/unitary/lending/lend_controller/test_set_admin_fee_lc.py b/tests/unitary/lending/lend_controller/test_set_admin_fee_lc.py new file mode 100644 index 00000000..bf21b0ef --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_set_admin_fee_lc.py @@ -0,0 +1,23 @@ +import boa +import pytest + +from tests.utils import filter_logs +from tests.utils.constants import WAD + + +@pytest.mark.parametrize("new_fee", [0, WAD // 2]) +def test_default_behavior(admin, controller, new_fee): + controller.set_admin_percentage(new_fee, sender=admin) + logs = filter_logs(controller, "SetAdminFee") + assert len(logs) == 1 and logs[-1].admin_fee == new_fee + assert controller.admin_fee() == new_fee + + +def test_more_than_max(admin, controller): + with boa.reverts(dev="admin fee higher than 100%"): + controller.set_admin_percentage(WAD + 1, sender=admin) + + +def test_unauthorized(controller): + with boa.reverts("only admin"): + controller.set_admin_percentage(1) diff --git a/tests/unitary/lending/lend_controller/test_set_borrow_cap_lc.py b/tests/unitary/lending/lend_controller/test_set_borrow_cap_lc.py new file mode 100644 index 00000000..b0367e66 --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_set_borrow_cap_lc.py @@ -0,0 +1,16 @@ +import boa +import pytest +from tests.utils import filter_logs + + +@pytest.mark.parametrize("new_cap", [0, 12345]) +def test_default_behavior(controller, admin, new_cap): + controller.set_borrow_cap(new_cap, sender=admin) + logs = filter_logs(controller, "SetBorrowCap") + assert len(logs) == 1 and logs[-1].borrow_cap == new_cap + assert controller.borrow_cap() == new_cap + + +def test_set_borrow_cap_non_admin_reverts(controller): + with boa.reverts("only admin"): + controller.set_borrow_cap(1) diff --git a/tests/unitary/lending/lend_controller/test_version_lc.py b/tests/unitary/lending/lend_controller/test_version_lc.py new file mode 100644 index 00000000..c0a09060 --- /dev/null +++ b/tests/unitary/lending/lend_controller/test_version_lc.py @@ -0,0 +1,5 @@ +from tests.utils.constants import __version__ + + +def test_default_behavior(controller): + assert controller.version() == f"{__version__}-lend" diff --git a/tests/unitary/lending/vault/test_admin_v.py b/tests/unitary/lending/vault/test_admin_v.py new file mode 100644 index 00000000..6b83c27a --- /dev/null +++ b/tests/unitary/lending/vault/test_admin_v.py @@ -0,0 +1,16 @@ +import boa + + +def test_admin(vault, factory, admin): + """Test that vault admin changes when factory admin changes.""" + # Check that vault admin is the same as factory admin + assert vault.admin() == factory.admin() == admin + + # Generate new admin + new_admin = boa.env.generate_address() + + # Change factory admin + factory.transfer_ownership(new_admin, sender=admin) + + # Check that vault admin is now the new factory admin + assert vault.admin() == factory.admin() == new_admin diff --git a/tests/unitary/lending/vault/test_asset_v.py b/tests/unitary/lending/vault/test_asset_v.py new file mode 100644 index 00000000..ffd30d37 --- /dev/null +++ b/tests/unitary/lending/vault/test_asset_v.py @@ -0,0 +1,3 @@ +def test_asset_returns_borrowed_token(vault, borrowed_token): + """Test that asset() returns the borrowed token.""" + assert vault.asset() == borrowed_token.address diff --git a/tests/unitary/lending/vault/test_borrow_apr_v.py b/tests/unitary/lending/vault/test_borrow_apr_v.py new file mode 100644 index 00000000..c97c8725 --- /dev/null +++ b/tests/unitary/lending/vault/test_borrow_apr_v.py @@ -0,0 +1,9 @@ +def test_borrow_apr_calculation(vault, amm): + """Test that borrow_apr correctly calculates annualized rate from AMM rate.""" + rate = 10**9 + amm.eval(f"self.rate = {10**9}") + seconds_in_year = 365 * 86400 # 31,536,000 seconds + expected_apr = rate * seconds_in_year + + actual_apr = vault.borrow_apr() + assert actual_apr == expected_apr diff --git a/tests/unitary/lending/vault/test_convert_to_assets_v.py b/tests/unitary/lending/vault/test_convert_to_assets_v.py new file mode 100644 index 00000000..7259f965 --- /dev/null +++ b/tests/unitary/lending/vault/test_convert_to_assets_v.py @@ -0,0 +1,48 @@ +def test_convert_to_assets(vault, controller, amm, make_debt): + """Test _convert_to_assets with is_floor=True and False.""" + shares = 100 * 10**18 + total_assets = vault.totalAssets() + total_supply = vault.totalSupply() + precision = vault.eval("self.precision") + dead_shares = vault.eval("DEAD_SHARES") + + # Calculate expected assets (floor) + numerator = shares * (total_assets * precision + 1) + denominator = (total_supply + dead_shares) * precision + + expected_assets_floor = numerator // denominator + actual_assets_floor = vault.eval(f"self._convert_to_assets({shares}, True)") + assert actual_assets_floor == expected_assets_floor + # Check that _is_floor=True by default + assert actual_assets_floor == vault.eval(f"self._convert_to_assets({shares})") + # Check external method + assert actual_assets_floor == vault.convertToAssets(shares) + + expected_assets_ceil = (numerator + denominator - 1) // denominator + actual_assets_ceil = vault.eval(f"self._convert_to_assets({shares}, False)") + assert actual_assets_ceil == expected_assets_ceil + + +def test_convert_to_assets_with_total_assets(vault, controller, borrowed_token): + """Test _convert_to_assets with custom _total_assets parameter.""" + shares = 100 * 10**18 + total_assets = 500 * 10 ** borrowed_token.decimals() + total_supply = vault.totalSupply() + precision = vault.eval("self.precision") + dead_shares = vault.eval("DEAD_SHARES") + + # Calculate expected assets using custom total_assets + numerator = shares * (total_assets * precision + 1) + denominator = (total_supply + dead_shares) * precision + + expected_assets_floor = numerator // denominator + actual_assets_floor = vault.eval( + f"self._convert_to_assets({shares}, True, {total_assets})" + ) + assert actual_assets_floor == expected_assets_floor + + expected_assets_ceil = (numerator + denominator - 1) // denominator + actual_assets_ceil = vault.eval( + f"self._convert_to_assets({shares}, False, {total_assets})" + ) + assert actual_assets_ceil == expected_assets_ceil diff --git a/tests/unitary/lending/vault/test_convert_to_shares_v.py b/tests/unitary/lending/vault/test_convert_to_shares_v.py new file mode 100644 index 00000000..89380e63 --- /dev/null +++ b/tests/unitary/lending/vault/test_convert_to_shares_v.py @@ -0,0 +1,48 @@ +def test_convert_to_shares(vault, controller, amm, borrowed_token, make_debt): + """Test _convert_to_shares with is_floor=True (default).""" + assets = 100 * 10 ** borrowed_token.decimals() + total_assets = vault.totalAssets() + total_supply = vault.totalSupply() + precision = vault.eval("self.precision") + dead_shares = vault.eval("DEAD_SHARES") + + # Calculate expected shares (floor) + numerator = (total_supply + dead_shares) * assets * precision + denominator = total_assets * precision + 1 + + expected_shares_floor = numerator // denominator + actual_shares_floor = vault.eval(f"self._convert_to_shares({assets}, True)") + assert actual_shares_floor == expected_shares_floor + # Check that _is_floor=True by default + assert actual_shares_floor == vault.eval(f"self._convert_to_shares({assets})") + # Check external method + assert actual_shares_floor == vault.convertToShares(assets) + + expected_shares_ceil = (numerator + denominator - 1) // denominator + actual_shares_ceil = vault.eval(f"self._convert_to_shares({assets}, False)") + assert actual_shares_ceil == expected_shares_ceil + + +def test_convert_to_shares_with_total_assets(vault, controller, borrowed_token): + """Test _convert_to_shares with custom _total_assets parameter.""" + assets = 100 * 10 ** borrowed_token.decimals() + total_assets = 500 * 10 ** borrowed_token.decimals() + total_supply = vault.totalSupply() + precision = vault.eval("self.precision") + dead_shares = vault.eval("DEAD_SHARES") + + # Calculate expected shares using custom total_assets + numerator = (total_supply + dead_shares) * assets * precision + denominator = total_assets * precision + 1 + + expected_shares_floor = numerator // denominator + actual_shares_floor = vault.eval( + f"self._convert_to_shares({assets}, True, {total_assets})" + ) + assert actual_shares_floor == expected_shares_floor + + expected_shares_ceil = (numerator + denominator - 1) // denominator + actual_shares_ceil = vault.eval( + f"self._convert_to_shares({assets}, False, {total_assets})" + ) + assert actual_shares_ceil == expected_shares_ceil diff --git a/tests/unitary/lending/vault/test_deposit_v.py b/tests/unitary/lending/vault/test_deposit_v.py new file mode 100644 index 00000000..5252da8e --- /dev/null +++ b/tests/unitary/lending/vault/test_deposit_v.py @@ -0,0 +1,154 @@ +import boa +import pytest +from tests.utils import filter_logs + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_deposit_basic(vault, controller, amm, monetary_policy, borrowed_token): + """Test basic deposit functionality - balances, rate, event.""" + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_balance = vault.net_deposits() + initial_total_supply = vault.totalSupply() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_amm_rate = amm.rate() + + assert amm.eval("self.rate_time") == 0 + + assets = 100 * 10 ** borrowed_token.decimals() + + # Give user tokens and approve vault + boa.deal(borrowed_token, boa.env.eoa, assets) + borrowed_token.approve(vault, assets) + + # Check preview matches + expected_shares = vault.previewDeposit(assets) + + # Increase rate by 1 + monetary_policy.set_rate(initial_amm_rate + 1) + + # Deposit assets + shares = vault.deposit(assets) + logs = filter_logs(vault, "Deposit") + + # Check shares match preview + assert shares == expected_shares + + # Check balances + assert vault.balanceOf(boa.env.eoa) == initial_sender_balance + shares + assert vault.net_deposits() == initial_balance + assets + assert vault.totalSupply() == initial_total_supply + shares + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) + assert borrowed_token.balanceOf(boa.env.eoa) == 0 + + # Check rate was saved + assert amm.eval("self.rate_time") > 0 + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa + assert logs[0].owner == boa.env.eoa + assert logs[0].assets == assets + assert logs[0].shares == shares + + +def test_deposit_with_receiver(vault, controller, amm, monetary_policy, borrowed_token): + """Test deposit with receiver argument - shares go to receiver, not sender.""" + # Generate receiver wallet + receiver = boa.env.generate_address() + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_receiver_balance = vault.balanceOf(receiver) + initial_balance = vault.net_deposits() + initial_total_supply = vault.totalSupply() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_amm_rate = amm.rate() + + assert amm.eval("self.rate_time") == 0 + + assets = 100 * 10 ** borrowed_token.decimals() + + # Give user tokens and approve vault + boa.deal(borrowed_token, boa.env.eoa, assets) + borrowed_token.approve(vault, assets) + + # Check preview matches + expected_shares = vault.previewDeposit(assets) + + # Increase rate by 1 + monetary_policy.set_rate(initial_amm_rate + 1) + + # Deposit assets with receiver + shares = vault.deposit(assets, receiver) + logs = filter_logs(vault, "Deposit") + + # Check shares match preview + assert shares == expected_shares + + # Check balances - shares go to receiver, not sender + assert ( + vault.balanceOf(boa.env.eoa) == initial_sender_balance + ) # Sender balance unchanged + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + shares + ) # Receiver gets shares + assert vault.net_deposits() == initial_balance + assets + assert vault.totalSupply() == initial_total_supply + shares + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) + assert borrowed_token.balanceOf(boa.env.eoa) == 0 + + # Check rate was saved + assert amm.eval("self.rate_time") > 0 + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct receiver + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa # Sender is the caller + assert logs[0].owner == receiver # Owner is the receiver + assert logs[0].assets == assets + assert logs[0].shares == shares + + +def test_deposit_need_more_assets_revert(vault, controller, amm, borrowed_token): + """Test deposit reverts with 'Need more assets' when total assets too low.""" + assert vault.totalAssets() == 0 + + # Small deposit that would make total assets < MIN_ASSETS + assets = vault.eval("MIN_ASSETS") - 1 + boa.deal(borrowed_token, boa.env.eoa, assets) + borrowed_token.approve(vault, assets) + + # Should revert with "Need more assets" + with boa.reverts("Need more assets"): + vault.deposit(assets) + + +def test_deposit_supply_limit_revert(vault, controller, amm, borrowed_token): + """Test deposit reverts with 'Supply limit' when exceeding max supply.""" + assert vault.totalAssets() == 0 + + # Set a low max supply + max_supply = 100 * 10 ** borrowed_token.decimals() # Just above current assets + vault.eval(f"self.maxSupply = {max_supply}") + + # Try to deposit more than allowed + assets = max_supply + 1 # More than the limit + boa.deal(borrowed_token, boa.env.eoa, assets) + borrowed_token.approve(vault, assets) + + # Should revert with "Supply limit" + with boa.reverts("Supply limit"): + vault.deposit(assets) diff --git a/tests/unitary/lending/vault/test_initialize_v.py b/tests/unitary/lending/vault/test_initialize_v.py new file mode 100644 index 00000000..4580e267 --- /dev/null +++ b/tests/unitary/lending/vault/test_initialize_v.py @@ -0,0 +1,46 @@ +import pytest +from tests.utils.constants import MAX_UINT256 + + +@pytest.fixture(scope="module") +def fresh_market( + proto, + borrowed_token, + collateral_token, + amm_A, + amm_fee, + loan_discount, + liquidation_discount, + price_oracle, + min_borrow_rate, + max_borrow_rate, +): + return proto.create_lending_market( + borrowed_token=borrowed_token, + collateral_token=collateral_token, + A=amm_A, + fee=amm_fee, + loan_discount=loan_discount, + liquidation_discount=liquidation_discount, + price_oracle=price_oracle, + name="Borrow Cap", + min_borrow_rate=min_borrow_rate, + max_borrow_rate=max_borrow_rate, + seed_amount=0, + ) + + +def test_default_behavior(fresh_market, collateral_token, borrowed_token, proto): + vault = fresh_market["vault"] + controller = fresh_market["controller"] + amm = fresh_market["amm"] + + assert vault.borrowed_token() == borrowed_token.address + assert vault.collateral_token() == collateral_token.address + assert vault.factory() == proto.lending_factory.address + assert vault.amm() == amm.address + assert vault.controller() == controller.address + assert vault.eval("self.precision") == 10 ** (18 - borrowed_token.decimals()) + assert vault.name() == "Curve Vault for " + borrowed_token.symbol() + assert vault.symbol() == "cv" + borrowed_token.symbol() + assert vault.maxSupply() == MAX_UINT256 diff --git a/tests/unitary/lending/vault/test_lend_apr_v.py b/tests/unitary/lending/vault/test_lend_apr_v.py new file mode 100644 index 00000000..4fbf812b --- /dev/null +++ b/tests/unitary/lending/vault/test_lend_apr_v.py @@ -0,0 +1,20 @@ +def test_lend_apr_calculation(vault, amm, controller): + """Test that lend_apr correctly calculates lending APR.""" + rate = 10**9 + borrowed_balance = controller.available_balance() + assert borrowed_balance > 0 + + debt = borrowed_balance // 2 + rate_mul = int(1.2 * 10**18) + _total_debt_rate_mul = int(1.1 * 10**18) + amm.eval(f"self.rate_mul = {rate_mul}") + controller.eval(f"core._total_debt.initial_debt = {debt}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + amm.eval(f"self.rate = {rate}") + + seconds_in_year = 365 * 86400 + debt = debt * rate_mul // _total_debt_rate_mul + expected_apr = rate * seconds_in_year * debt // (debt + borrowed_balance) + + actual_apr = vault.lend_apr() + assert actual_apr == expected_apr diff --git a/tests/unitary/lending/vault/test_max_deposit_v.py b/tests/unitary/lending/vault/test_max_deposit_v.py new file mode 100644 index 00000000..9c231d57 --- /dev/null +++ b/tests/unitary/lending/vault/test_max_deposit_v.py @@ -0,0 +1,43 @@ +def test_max_deposit_unlimited(vault): + """Test maxDeposit when maxSupply is unlimited (MAX_UINT256).""" + # Ensure maxSupply is unlimited + vault.eval("self.maxSupply = max_value(uint256)") + + actual_max = vault.maxDeposit(vault.address) + assert actual_max == vault.eval("max_value(uint256)") + + +def test_max_deposit_under_limit(vault, controller, amm): + """Test maxDeposit when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 0 + + # Set a specific max supply + max_supply = total_assets + 1 + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxDeposit(vault.address) == 1 + + +def test_max_deposit_above_limit(vault, controller, amm): + """Test maxDeposit when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 1 + + # Set a specific max supply + max_supply = total_assets - 1 + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxDeposit(vault.address) == 0 + + +def test_max_deposit_at_limit(vault, controller, amm): + """Test maxDeposit when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 0 + + # Set a specific max supply + max_supply = total_assets + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxDeposit(vault.address) == 0 diff --git a/tests/unitary/lending/vault/test_max_mint_v.py b/tests/unitary/lending/vault/test_max_mint_v.py new file mode 100644 index 00000000..582fb041 --- /dev/null +++ b/tests/unitary/lending/vault/test_max_mint_v.py @@ -0,0 +1,44 @@ +def test_max_mint_unlimited(vault): + """Test maxMint when maxSupply is unlimited (MAX_UINT256).""" + # Ensure maxSupply is unlimited + vault.eval("self.maxSupply = max_value(uint256)") + + actual_max = vault.maxMint(vault.address) + assert actual_max == vault.eval("max_value(uint256)") + + +def test_max_mint_under_limit(vault, controller, amm, borrowed_token): + """Test maxMint when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 0 + + # Set a specific max supply + assets = 100 * borrowed_token.decimals() + max_supply = total_assets + assets + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxMint(vault.address) == vault.convertToShares(assets) + + +def test_max_mint_above_limit(vault, controller, amm): + """Test maxMint when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 1 + + # Set a specific max supply + max_supply = total_assets - 1 + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxMint(vault.address) == 0 + + +def test_max_mint_at_limit(vault, controller, amm): + """Test maxMint when maxSupply is limited.""" + total_assets = vault.totalAssets() + assert total_assets > 0 + + # Set a specific max supply + max_supply = total_assets + vault.eval(f"self.maxSupply = {max_supply}") + + assert vault.maxMint(vault.address) == 0 diff --git a/tests/unitary/lending/vault/test_max_redeem_v.py b/tests/unitary/lending/vault/test_max_redeem_v.py new file mode 100644 index 00000000..a8c8a270 --- /dev/null +++ b/tests/unitary/lending/vault/test_max_redeem_v.py @@ -0,0 +1,72 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_max_redeem_user_balance_limited( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxRedeem when user balance is the limiting factor.""" + deposit_into_vault() + + # maxRedeem should be limited by user's balance + user_balance = vault.balanceOf(boa.env.eoa) + expected_max = user_balance + actual_max = vault.maxRedeem(boa.env.eoa) + assert actual_max == expected_max + + +def test_max_redeem_controller_limited( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxRedeem when controller liquidity is the limiting factor.""" + deposit_into_vault() + + # Reduce controller balance to be less than user's position + controller_balance = controller.available_balance() + # Increase lent to reduce borrowed_balance + controller.eval(f"core.lent = {controller_balance // 2}") + limited_balance = controller_balance // 2 + + # maxRedeem should be limited by controller balance + expected_max = vault.eval(f"self._convert_to_shares({limited_balance}, False)") + actual_max = vault.maxRedeem(boa.env.eoa) + assert actual_max == expected_max + + +def test_max_redeem_zero_balance( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxRedeem when user has no shares.""" + # Load borrowed tokens into vault + deposit_into_vault(user=boa.env.generate_address()) + + # User has no shares + user_balance = vault.balanceOf(boa.env.eoa) + assert user_balance == 0 + + # maxRedeem should return 0 + actual_max = vault.maxRedeem(boa.env.eoa) + assert actual_max == 0 + + +def test_max_redeem_zero_controller_balance( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxRedeem when controller has no liquidity.""" + deposit_into_vault() + + # Set controller balance to 0 by setting lent = borrowed_balance + borrowed_balance = controller.available_balance() + controller.eval(f"core.lent = {borrowed_balance}") + + # maxRedeem should return 0 (limited by controller balance) + actual_max = vault.maxRedeem(boa.env.eoa) + assert actual_max == 0 diff --git a/tests/unitary/lending/vault/test_max_withdraw_v.py b/tests/unitary/lending/vault/test_max_withdraw_v.py new file mode 100644 index 00000000..dd416b65 --- /dev/null +++ b/tests/unitary/lending/vault/test_max_withdraw_v.py @@ -0,0 +1,70 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_max_withdraw_user_balance_limited( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxWithdraw when user balance is the limiting factor.""" + deposit_into_vault() + + # maxWithdraw should be limited by user's balance + user_balance = vault.balanceOf(boa.env.eoa) + expected_max = vault.convertToAssets(user_balance) + actual_max = vault.maxWithdraw(boa.env.eoa) + assert actual_max == expected_max + + +def test_max_withdraw_controller_limited( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxWithdraw when controller liquidity is the limiting factor.""" + deposit_into_vault() + + # Reduce controller balance to be less than user's position + controller_balance = controller.available_balance() + # Increase lent to reduce borrowed_balance + controller.eval(f"core.lent = {controller_balance // 2}") + limited_balance = controller_balance // 2 + + # maxWithdraw should be limited by controller balance + actual_max = vault.maxWithdraw(boa.env.eoa) + assert actual_max == limited_balance + + +def test_max_withdraw_zero_balance( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxWithdraw when user has no shares.""" + # Load borrowed tokens into vault + deposit_into_vault(boa.env.generate_address()) + + # User has no shares + user_balance = vault.balanceOf(boa.env.eoa) + assert user_balance == 0 + + # maxWithdraw should return 0 + actual_max = vault.maxWithdraw(boa.env.eoa) + assert actual_max == 0 + + +def test_max_withdraw_zero_controller_balance( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test maxWithdraw when controller has no liquidity.""" + deposit_into_vault() + + # Set controller balance to 0 by setting lent = borrowed_balance + controller.eval(f"core.lent = {controller.available_balance()}") + + # maxWithdraw should return 0 (limited by controller balance) + actual_max = vault.maxWithdraw(boa.env.eoa) + assert actual_max == 0 diff --git a/tests/unitary/lending/vault/test_mint_v.py b/tests/unitary/lending/vault/test_mint_v.py new file mode 100644 index 00000000..a98b6c3b --- /dev/null +++ b/tests/unitary/lending/vault/test_mint_v.py @@ -0,0 +1,156 @@ +import boa +import pytest +from tests.utils import filter_logs + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_mint_basic(vault, controller, amm, monetary_policy, borrowed_token): + """Test basic mint functionality - balances, rate, event.""" + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_balance = vault.net_deposits() + initial_total_supply = vault.totalSupply() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_amm_rate = amm.rate() + + assert amm.eval("self.rate_time") == 0 + + shares = 100 * 10**18 + + # Check preview matches + expected_assets = vault.previewMint(shares) + + # Give user tokens and approve vault + boa.deal(borrowed_token, boa.env.eoa, expected_assets) + borrowed_token.approve(vault, expected_assets) + + # Increase rate by 1 + monetary_policy.set_rate(initial_amm_rate + 1) + + # Mint shares + assets = vault.mint(shares) + logs = filter_logs(vault, "Deposit") + + # Check assets match preview + assert assets == expected_assets + + # Check balances + assert vault.balanceOf(boa.env.eoa) == initial_sender_balance + shares + assert vault.net_deposits() == initial_balance + assets + assert vault.totalSupply() == initial_total_supply + shares + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) + assert borrowed_token.balanceOf(boa.env.eoa) == 0 + + # Check rate was saved + assert amm.eval("self.rate_time") > 0 + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa + assert logs[0].owner == boa.env.eoa + assert logs[0].assets == assets + assert logs[0].shares == shares + + +def test_mint_with_receiver(vault, controller, amm, monetary_policy, borrowed_token): + """Test mint with receiver argument - shares go to receiver, not sender.""" + # Generate receiver wallet + receiver = boa.env.generate_address() + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_receiver_balance = vault.balanceOf(receiver) + initial_balance = vault.net_deposits() + initial_total_supply = vault.totalSupply() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_amm_rate = amm.rate() + + assert amm.eval("self.rate_time") == 0 + + shares = 100 * 10**18 + + # Check preview matches + expected_assets = vault.previewMint(shares) + + # Give user tokens and approve vault + boa.deal(borrowed_token, boa.env.eoa, expected_assets) + borrowed_token.approve(vault, expected_assets) + + # Increase rate by 1 + monetary_policy.set_rate(initial_amm_rate + 1) + + # Mint shares with receiver + assets = vault.mint(shares, receiver) + logs = filter_logs(vault, "Deposit") + + # Check assets match preview + assert assets == expected_assets + + # Check balances - shares go to receiver, not sender + assert ( + vault.balanceOf(boa.env.eoa) == initial_sender_balance + ) # Sender balance unchanged + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + shares + ) # Receiver gets shares + assert vault.net_deposits() == initial_balance + assets + assert vault.totalSupply() == initial_total_supply + shares + assert ( + borrowed_token.balanceOf(controller.address) + == initial_controller_balance + assets + ) + assert borrowed_token.balanceOf(boa.env.eoa) == 0 + + # Check rate was saved + assert amm.eval("self.rate_time") > 0 + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct receiver + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa # Sender is the caller + assert logs[0].owner == receiver # Owner is the receiver + assert logs[0].assets == assets + assert logs[0].shares == shares + + +def test_mint_need_more_assets_revert(vault, controller, amm, borrowed_token): + """Test mint reverts with 'Need more assets' when total assets too low.""" + assert vault.totalAssets() == 0 + + # Small mint that would make total assets < MIN_ASSETS + assets = vault.eval("MIN_ASSETS") - 1 + shares = vault.convertToShares(assets) # Very small amount + boa.deal(borrowed_token, boa.env.eoa, assets) + borrowed_token.approve(vault, assets) + + # Should revert with "Need more assets" + with boa.reverts("Need more assets"): + vault.mint(shares) + + +def test_mint_supply_limit_revert(vault, controller, amm, borrowed_token): + """Test mint reverts with 'Supply limit' when exceeding max supply.""" + assert vault.totalAssets() == 0 + + # Set a low max supply + max_supply = 100 * 10 ** borrowed_token.decimals() # Just above current assets + vault.eval(f"self.maxSupply = {max_supply}") + + # Try to mint more than allowed + shares = vault.maxMint(boa.env.eoa) + 1 # More than the limit + expected_assets = vault.previewMint(shares) + boa.deal(borrowed_token, boa.env.eoa, expected_assets) + borrowed_token.approve(vault, expected_assets) + + # Should revert with "Supply limit" + with boa.reverts("Supply limit"): + vault.mint(shares) diff --git a/tests/unitary/lending/vault/test_preview_deposit_v.py b/tests/unitary/lending/vault/test_preview_deposit_v.py new file mode 100644 index 00000000..d87b33a1 --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_deposit_v.py @@ -0,0 +1,6 @@ +def test_preview_deposit(vault, controller, amm, borrowed_token, make_debt): + """Test previewDeposit returns correct shares for given assets.""" + assets = 100 * 10 ** borrowed_token.decimals() + assert vault.previewDeposit(assets) == vault.eval( + f"self._convert_to_shares({assets})" + ) diff --git a/tests/unitary/lending/vault/test_preview_mint_v.py b/tests/unitary/lending/vault/test_preview_mint_v.py new file mode 100644 index 00000000..abe6252c --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_mint_v.py @@ -0,0 +1,6 @@ +def test_preview_mint(vault, controller, amm, borrowed_token, make_debt): + """Test previewMint returns correct assets for given shares.""" + shares = 100 * 10 ** borrowed_token.decimals() + assert vault.previewMint(shares) == vault.eval( + f"self._convert_to_assets({shares}, False)" + ) diff --git a/tests/unitary/lending/vault/test_preview_redeem_v.py b/tests/unitary/lending/vault/test_preview_redeem_v.py new file mode 100644 index 00000000..6277938b --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_redeem_v.py @@ -0,0 +1,51 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_preview_redeem(vault, controller, amm, borrowed_token, deposit_into_vault): + """Test previewRedeem returns correct assets for given shares.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + shares = vault.convertToShares(assets) + + assert vault.previewRedeem(shares) == vault.eval( + f"self._convert_to_assets({shares})" + ) + + +def test_preview_redeem_assert_revert( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test previewRedeem reverts when assets > borrowed_balance.""" + # Create vault state by depositing + deposit_into_vault() + + # Try to preview redeem more than available borrowed_balance + borrowed_balance = controller.available_balance() + shares = vault.convertToShares(borrowed_balance + 1) # More than available + + # Should revert due to assert in previewRedeem + with boa.reverts(): + vault.previewRedeem(shares) + + +def test_preview_redeem_zero_supply(vault, controller, amm, borrowed_token): + """Test previewRedeem when totalSupply is 0.""" + # Vault has no shares + assert vault.totalSupply() == 0 + + # Should return 0 for any shares + assert vault.previewRedeem(0) == 0 + + # Should revert for non-zero shares + with boa.reverts(): + vault.previewRedeem(1) diff --git a/tests/unitary/lending/vault/test_preview_withdraw_v.py b/tests/unitary/lending/vault/test_preview_withdraw_v.py new file mode 100644 index 00000000..3a6bd0b9 --- /dev/null +++ b/tests/unitary/lending/vault/test_preview_withdraw_v.py @@ -0,0 +1,36 @@ +import boa +import pytest + + +@pytest.fixture(scope="module") +def seed_liquidity(): + """Default liquidity amount used to seed markets at creation time. + Override in tests to customize seeding. + """ + return 0 + + +def test_preview_withdraw(vault, controller, amm, borrowed_token, deposit_into_vault): + """Test previewWithdraw returns correct shares for given assets.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + assert vault.previewWithdraw(assets) == vault.eval( + f"self._convert_to_shares({assets}, False)" + ) + + +def test_preview_withdraw_assert_revert( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test previewWithdraw reverts when assets > borrowed_balance.""" + # Create vault state by depositing + deposit_into_vault() + + # Try to preview withdraw more than available borrowed_balance + borrowed_balance = controller.available_balance() + assets = borrowed_balance + 1 # More than available + + # Should revert due to assert in previewWithdraw + with boa.reverts(): + vault.previewWithdraw(assets) diff --git a/tests/unitary/lending/vault/test_price_per_share_v.py b/tests/unitary/lending/vault/test_price_per_share_v.py new file mode 100644 index 00000000..4755ef73 --- /dev/null +++ b/tests/unitary/lending/vault/test_price_per_share_v.py @@ -0,0 +1,32 @@ +def test_price_per_share(vault, controller, amm, make_debt): + """Test pricePerShare with is_floor=True and False.""" + total_assets = vault.totalAssets() + total_supply = vault.totalSupply() + precision = vault.eval("self.precision") + dead_shares = vault.eval("DEAD_SHARES") + + # Calculate expected price per share (floor) + numerator = 10**18 * (total_assets * precision + 1) + denominator = total_supply + dead_shares + + expected_pps_floor = numerator // denominator + actual_pps_floor = vault.pricePerShare(True) + assert actual_pps_floor == expected_pps_floor + # Check that _is_floor=True by default + assert actual_pps_floor == vault.pricePerShare() + # Check that it's a view function + assert actual_pps_floor == vault.pricePerShare(True) + + expected_pps_ceil = (numerator + denominator - 1) // denominator + actual_pps_ceil = vault.pricePerShare(False) + assert actual_pps_ceil == expected_pps_ceil + + +def test_price_per_share_zero_supply(vault): + """Test pricePerShare when totalSupply is zero.""" + # Ensure zero supply + vault.eval("self.totalSupply = 0") + + expected_pps = 10**18 // vault.eval("DEAD_SHARES") + actual_pps = vault.pricePerShare() + assert actual_pps == expected_pps diff --git a/tests/unitary/lending/vault/test_redeem_v.py b/tests/unitary/lending/vault/test_redeem_v.py new file mode 100644 index 00000000..978bb7a8 --- /dev/null +++ b/tests/unitary/lending/vault/test_redeem_v.py @@ -0,0 +1,323 @@ +import boa +from tests.utils import filter_logs + + +def test_redeem_basic( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test basic redeem functionality - balances, rate, event.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + shares_to_redeem = initial_sender_balance // 2 + + # Check preview matches + expected_assets = vault.previewRedeem(shares_to_redeem) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Redeem shares + assets_redeemed = vault.redeem(shares_to_redeem) + logs = filter_logs(vault, "Withdraw") + + # Check assets match preview + assert assets_redeemed == expected_assets + + # Check balances + assert vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares_to_redeem + assert vault.totalSupply() == initial_total_supply - shares_to_redeem + assert vault.net_deposits() == initial_balance - assets_redeemed + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_redeemed + ) + assert ( + borrowed_token.balanceOf(boa.env.eoa) + == initial_sender_token_balance + assets_redeemed + ) + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa + assert logs[0].receiver == boa.env.eoa + assert logs[0].owner == boa.env.eoa + assert logs[0].assets == assets_redeemed + assert logs[0].shares == shares_to_redeem + + +def test_redeem_with_receiver( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test redeem with receiver argument - assets go to receiver, not sender.""" + # Generate receiver wallet + receiver = boa.env.generate_address() + + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_receiver_balance = vault.balanceOf(receiver) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) + initial_receiver_token_balance = borrowed_token.balanceOf(receiver) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + shares_to_redeem = initial_sender_balance // 2 + + # Check preview matches + expected_assets = vault.previewRedeem(shares_to_redeem) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Redeem shares with receiver + assets_redeemed = vault.redeem(shares_to_redeem, receiver) + logs = filter_logs(vault, "Withdraw") + + # Check assets match preview + assert assets_redeemed == expected_assets + + # Check balances - assets go to receiver, not sender + assert ( + vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares_to_redeem + ) # Sender shares burned + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares + assert vault.totalSupply() == initial_total_supply - shares_to_redeem + assert vault.net_deposits() == initial_balance - assets_redeemed + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_redeemed + ) + assert ( + borrowed_token.balanceOf(boa.env.eoa) == initial_sender_token_balance + ) # Sender gets no assets + assert ( + borrowed_token.balanceOf(receiver) + == initial_receiver_token_balance + assets_redeemed + ) # Receiver gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct receiver + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa # Sender is the caller + assert logs[0].receiver == receiver # Receiver gets the assets + assert logs[0].owner == boa.env.eoa # Owner is the sender + assert logs[0].assets == assets_redeemed + assert logs[0].shares == shares_to_redeem + + +def test_redeem_with_owner( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test redeem with owner argument - owner's shares are burned.""" + # Generate owner and caller wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + initial_owner_balance = vault.balanceOf(owner) + initial_caller_balance = vault.balanceOf(caller) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_caller_token_balance = borrowed_token.balanceOf(caller) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + shares_to_redeem = initial_owner_balance // 2 + + # Check preview matches + expected_assets = vault.previewRedeem(shares_to_redeem) + + # Give owner approval to caller + vault.approve(caller, shares_to_redeem, sender=owner) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Redeem shares with owner (caller redeems owner's shares) + with boa.env.prank(caller): + assets_redeemed = vault.redeem(shares_to_redeem, caller, owner) + logs = filter_logs(vault, "Withdraw") + + # Check assets match preview + assert assets_redeemed == expected_assets + + # Check balances - owner's shares burned, caller gets assets + assert ( + vault.balanceOf(owner) == initial_owner_balance - shares_to_redeem + ) # Owner's shares burned + assert vault.balanceOf(caller) == initial_caller_balance # Caller gets no shares + assert vault.totalSupply() == initial_total_supply - shares_to_redeem + assert vault.net_deposits() == initial_balance - assets_redeemed + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_redeemed + ) + assert ( + borrowed_token.balanceOf(caller) + == initial_caller_token_balance + assets_redeemed + ) # Caller gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct parameters + assert len(logs) == 1 + assert logs[0].sender == caller # Sender is the caller + assert logs[0].receiver == caller # Receiver is the caller + assert logs[0].owner == owner # Owner is the owner + assert logs[0].assets == assets_redeemed + assert logs[0].shares == shares_to_redeem + + +def test_redeem_with_owner_and_receiver( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test redeem with both owner and receiver - owner's shares burned, receiver gets assets.""" + # Generate owner, caller, and receiver wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + receiver = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + initial_owner_balance = vault.balanceOf(owner) + initial_caller_balance = vault.balanceOf(caller) + initial_receiver_balance = vault.balanceOf(receiver) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_caller_token_balance = borrowed_token.balanceOf(caller) + initial_receiver_token_balance = borrowed_token.balanceOf(receiver) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + shares_to_redeem = initial_owner_balance // 2 + + # Check preview matches + expected_assets = vault.previewRedeem(shares_to_redeem) + + # Give owner approval to caller + vault.approve(caller, shares_to_redeem, sender=owner) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Redeem shares with owner and receiver (caller redeems owner's shares to receiver) + with boa.env.prank(caller): + assets_redeemed = vault.redeem(shares_to_redeem, receiver, owner) + logs = filter_logs(vault, "Withdraw") + + # Check assets match preview + assert assets_redeemed == expected_assets + + # Check balances - owner's shares burned, receiver gets assets + assert ( + vault.balanceOf(owner) == initial_owner_balance - shares_to_redeem + ) # Owner's shares burned + assert vault.balanceOf(caller) == initial_caller_balance # Caller gets no shares + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares + assert vault.totalSupply() == initial_total_supply - shares_to_redeem + assert vault.net_deposits() == initial_balance - assets_redeemed + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_redeemed + ) + assert ( + borrowed_token.balanceOf(caller) == initial_caller_token_balance + ) # Caller gets no assets + assert ( + borrowed_token.balanceOf(receiver) + == initial_receiver_token_balance + assets_redeemed + ) # Receiver gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct parameters + assert len(logs) == 1 + assert logs[0].sender == caller # Sender is the caller + assert logs[0].receiver == receiver # Receiver is the receiver + assert logs[0].owner == owner # Owner is the owner + assert logs[0].assets == assets_redeemed + assert logs[0].shares == shares_to_redeem + + +def test_redeem_need_more_assets_revert( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test redeem reverts with 'Need more assets' when total assets too low.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + # Try to redeem more than available (would leave vault with < MIN_ASSETS) + total_assets = vault.totalAssets() + shares_to_redeem = vault.convertToShares( + total_assets - vault.eval("MIN_ASSETS") + 1 + ) + + # Should revert with "Need more assets" + with boa.reverts("Need more assets"): + vault.redeem(shares_to_redeem) + + +def test_redeem_insufficient_allowance_revert( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test redeem reverts when allowance < shares.""" + # Generate owner and caller wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + # Give owner some allowance to caller, but less than needed + shares_to_redeem = vault.balanceOf(owner) // 2 + insufficient_allowance = shares_to_redeem - 1 # Less than needed + vault.approve(caller, insufficient_allowance, sender=owner) + + # Try to redeem with insufficient allowance + with boa.env.prank(caller): + with boa.reverts(): + vault.redeem(shares_to_redeem, caller, owner) diff --git a/tests/unitary/lending/vault/test_set_max_supply_v.py b/tests/unitary/lending/vault/test_set_max_supply_v.py new file mode 100644 index 00000000..fa88d084 --- /dev/null +++ b/tests/unitary/lending/vault/test_set_max_supply_v.py @@ -0,0 +1,40 @@ +import boa +from tests.utils import filter_logs + +NEW_MAX_SUPPLY = 1000000 * 10**18 + + +def test_set_max_supply_by_admin(vault, admin): + """Test that factory admin can set max supply.""" + + # Set max supply as factory admin + vault.set_max_supply(NEW_MAX_SUPPLY, sender=admin) + logs = filter_logs(vault, "SetMaxSupply") + + # Verify max supply was updated + assert vault.maxSupply() == NEW_MAX_SUPPLY + + # Verify log was emitted + assert len(logs) == 1 and logs[-1].max_supply == NEW_MAX_SUPPLY + + +def test_set_max_supply_by_factory(vault, proto): + """Test that factory address can set max supply.""" + + # Set max supply as factory + vault.set_max_supply(NEW_MAX_SUPPLY, sender=proto.lending_factory.address) + logs = filter_logs(vault, "SetMaxSupply") + + # Verify max supply was updated + assert vault.maxSupply() == NEW_MAX_SUPPLY + + # Verify log was emitted + assert len(logs) == 1 and logs[-1].max_supply == NEW_MAX_SUPPLY + + +def test_set_max_supply_unauthorized_reverts(vault): + """Test that unauthorized users cannot set max supply.""" + + # Attempt to set max supply as unauthorized user + with boa.reverts(): + vault.set_max_supply(NEW_MAX_SUPPLY) diff --git a/tests/unitary/lending/vault/test_total_assets_v.py b/tests/unitary/lending/vault/test_total_assets_v.py new file mode 100644 index 00000000..c6ea3b28 --- /dev/null +++ b/tests/unitary/lending/vault/test_total_assets_v.py @@ -0,0 +1,19 @@ +def test_total_assets_calculation(vault, controller, amm): + """Test that _total_assets correctly calculates total assets.""" + # Set specific debt value + + borrowed_balance = controller.available_balance() + debt = borrowed_balance // 2 + rate_mul = int(1.2 * 10**18) + _total_debt_rate_mul = int(1.1 * 10**18) + amm.eval(f"self.rate_mul = {rate_mul}") + controller.eval(f"core._total_debt.initial_debt = {debt}") + controller.eval(f"core._total_debt.rate_mul = {_total_debt_rate_mul}") + + expected_total = borrowed_balance + debt * rate_mul // _total_debt_rate_mul + actual_total = vault.eval("self._total_assets()") + + assert actual_total == expected_total + + # Check that external totalAssets() returns the same as internal _total_assets() + assert actual_total == vault.totalAssets() diff --git a/tests/unitary/lending/vault/test_withdraw_v.py b/tests/unitary/lending/vault/test_withdraw_v.py new file mode 100644 index 00000000..82e26bf7 --- /dev/null +++ b/tests/unitary/lending/vault/test_withdraw_v.py @@ -0,0 +1,327 @@ +import boa +from tests.utils import filter_logs + + +def test_withdraw_basic( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test basic withdraw functionality - balances, rate, event.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + assets_to_withdraw = assets // 2 + + # Check preview matches + expected_shares = vault.previewWithdraw(assets_to_withdraw) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Withdraw assets + shares = vault.withdraw(assets_to_withdraw) + logs = filter_logs(vault, "Withdraw") + + # Check shares match preview + assert shares == expected_shares + + # Check balances + assert vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares + assert vault.totalSupply() == initial_total_supply - shares + assert vault.net_deposits() == initial_balance - assets_to_withdraw + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_to_withdraw + ) + assert ( + borrowed_token.balanceOf(boa.env.eoa) + == initial_sender_token_balance + assets_to_withdraw + ) + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa + assert logs[0].receiver == boa.env.eoa + assert logs[0].owner == boa.env.eoa + assert logs[0].assets == assets_to_withdraw + assert logs[0].shares == shares + + +def test_withdraw_with_receiver( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test withdraw with receiver argument - assets go to receiver, not sender.""" + # Generate receiver wallet + receiver = boa.env.generate_address() + + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + initial_sender_balance = vault.balanceOf(boa.env.eoa) + initial_receiver_balance = vault.balanceOf(receiver) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_sender_token_balance = borrowed_token.balanceOf(boa.env.eoa) + initial_receiver_token_balance = borrowed_token.balanceOf(receiver) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + assets_to_withdraw = assets // 2 + + # Check preview matches + expected_shares = vault.previewWithdraw(assets_to_withdraw) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Withdraw assets with receiver + shares = vault.withdraw(assets_to_withdraw, receiver) + logs = filter_logs(vault, "Withdraw") + + # Check shares match preview + assert shares == expected_shares + + # Check balances - assets go to receiver, not sender + assert ( + vault.balanceOf(boa.env.eoa) == initial_sender_balance - shares + ) # Sender shares burned + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares + assert vault.totalSupply() == initial_total_supply - shares + assert vault.net_deposits() == initial_balance - assets_to_withdraw + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_to_withdraw + ) + assert ( + borrowed_token.balanceOf(boa.env.eoa) == initial_sender_token_balance + ) # Sender gets no assets + assert ( + borrowed_token.balanceOf(receiver) + == initial_receiver_token_balance + assets_to_withdraw + ) # Receiver gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct receiver + assert len(logs) == 1 + assert logs[0].sender == boa.env.eoa # Sender is the caller + assert logs[0].receiver == receiver # Receiver gets the assets + assert logs[0].owner == boa.env.eoa # Owner is the sender + assert logs[0].assets == assets_to_withdraw + assert logs[0].shares == shares + + +def test_withdraw_with_owner( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test withdraw with owner argument - owner's shares are burned.""" + # Generate owner and caller wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + initial_owner_balance = vault.balanceOf(owner) + initial_caller_balance = vault.balanceOf(caller) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_caller_token_balance = borrowed_token.balanceOf(caller) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + assets_to_withdraw = assets // 2 + + # Check preview matches + expected_shares = vault.previewWithdraw(assets_to_withdraw) + + # Give owner approval to caller + vault.approve(caller, expected_shares, sender=owner) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Withdraw assets with owner (caller withdraws owner's shares) + with boa.env.prank(caller): + shares = vault.withdraw(assets_to_withdraw, caller, owner) + logs = filter_logs(vault, "Withdraw") + + # Check shares match preview + assert shares == expected_shares + + # Check balances - owner's shares burned, caller gets assets + assert ( + vault.balanceOf(owner) == initial_owner_balance - shares + ) # Owner's shares burned + assert vault.balanceOf(caller) == initial_caller_balance # Caller gets no shares + assert vault.totalSupply() == initial_total_supply - shares + assert vault.net_deposits() == initial_balance - assets_to_withdraw + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_to_withdraw + ) + assert ( + borrowed_token.balanceOf(caller) + == initial_caller_token_balance + assets_to_withdraw + ) # Caller gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct parameters + assert len(logs) == 1 + assert logs[0].sender == caller # Sender is the caller + assert logs[0].receiver == caller # Receiver is the caller + assert logs[0].owner == owner # Owner is the owner + assert logs[0].assets == assets_to_withdraw + assert logs[0].shares == shares + + +def test_withdraw_with_owner_and_receiver( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test withdraw with both owner and receiver - owner's shares burned, receiver gets assets.""" + # Generate owner, caller, and receiver wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + receiver = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + initial_owner_balance = vault.balanceOf(owner) + initial_caller_balance = vault.balanceOf(caller) + initial_receiver_balance = vault.balanceOf(receiver) + initial_total_supply = vault.totalSupply() + initial_balance = vault.net_deposits() + initial_controller_balance = borrowed_token.balanceOf(controller.address) + initial_owner_token_balance = borrowed_token.balanceOf(owner) + initial_caller_token_balance = borrowed_token.balanceOf(caller) + initial_receiver_token_balance = borrowed_token.balanceOf(receiver) + + initial_amm_rate = amm.rate() + initial_amm_rate_time = amm.eval("self.rate_time") + + assets_to_withdraw = assets // 2 + + # Check preview matches + expected_shares = vault.previewWithdraw(assets_to_withdraw) + + # Give owner approval to caller + vault.approve(caller, expected_shares, sender=owner) + + # Increase rate_time and rate by 1 + boa.env.time_travel(1) + monetary_policy.set_rate(initial_amm_rate + 1) + + # Withdraw assets with owner and receiver (caller withdraws owner's shares to receiver) + with boa.env.prank(caller): + shares = vault.withdraw(assets_to_withdraw, receiver, owner) + logs = filter_logs(vault, "Withdraw") + + # Check shares match preview + assert shares == expected_shares + + # Check balances - owner's shares burned, receiver gets assets + assert ( + vault.balanceOf(owner) == initial_owner_balance - shares + ) # Owner's shares burned + assert vault.balanceOf(caller) == initial_caller_balance # Caller gets no shares + assert ( + vault.balanceOf(receiver) == initial_receiver_balance + ) # Receiver gets no shares + assert vault.totalSupply() == initial_total_supply - shares + assert vault.net_deposits() == initial_balance - assets_to_withdraw + assert ( + borrowed_token.balanceOf(controller) + == initial_controller_balance - assets_to_withdraw + ) + assert ( + borrowed_token.balanceOf(owner) == initial_owner_token_balance + ) # Owner gets no assets + assert ( + borrowed_token.balanceOf(caller) == initial_caller_token_balance + ) # Caller gets no assets + assert ( + borrowed_token.balanceOf(receiver) + == initial_receiver_token_balance + assets_to_withdraw + ) # Receiver gets assets + + # Check rate was saved + assert amm.eval("self.rate_time") > initial_amm_rate_time + assert amm.rate() == initial_amm_rate + 1 + + # Check event was emitted with correct parameters + assert len(logs) == 1 + assert logs[0].sender == caller # Sender is the caller + assert logs[0].receiver == receiver # Receiver is the receiver + assert logs[0].owner == owner # Owner is the owner + assert logs[0].assets == assets_to_withdraw + assert logs[0].shares == shares + + +def test_withdraw_need_more_assets_revert( + vault, controller, amm, borrowed_token, deposit_into_vault +): + """Test withdraw reverts with 'Need more assets' when total assets too low.""" + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(assets=assets) + + # Try to withdraw more than available (would leave vault with < MIN_ASSETS) + total_assets = vault.totalAssets() + withdraw_amount = ( + total_assets - vault.eval("MIN_ASSETS") + 1 + ) # Would leave < MIN_ASSETS + + # Should revert with "Need more assets" + with boa.reverts("Need more assets"): + vault.withdraw(withdraw_amount) + + +def test_withdraw_insufficient_allowance_revert( + vault, controller, amm, monetary_policy, borrowed_token, deposit_into_vault +): + """Test withdraw reverts when allowance < shares.""" + # Generate owner and caller wallets + owner = boa.env.generate_address() + caller = boa.env.generate_address() + + # Deposit for owner + assets = 100 * 10 ** borrowed_token.decimals() + deposit_into_vault(user=owner, assets=assets) + + # Give owner some allowance to caller, but less than needed + expected_shares = vault.previewWithdraw(assets) + insufficient_allowance = expected_shares - 1 # Less than needed + vault.approve(caller, insufficient_allowance, sender=owner) + + # Try to withdraw with insufficient allowance + with boa.env.prank(caller): + with boa.reverts(): + vault.withdraw(assets, caller, owner) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000..63ec703f --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,19 @@ +import boa + +from tests.utils.constants import MAX_UINT256 + + +def mint_for_testing(token, to, amount): + # DO NOT USE: this is an old function for backwards compatibility. + # When you need this just inline this boa behavior. + boa.deal(token, to, token.balanceOf(to) + amount) + + +def filter_logs(contract, event_name, _strict=False): + return [ + e for e in contract.get_logs(strict=_strict) if type(e).__name__ == event_name + ] + + +def max_approve(token, spender, *args, **kwargs): + token.approve(spender, MAX_UINT256, *args, **kwargs) diff --git a/tests/utils/constants.py b/tests/utils/constants.py new file mode 100644 index 00000000..ff8781be --- /dev/null +++ b/tests/utils/constants.py @@ -0,0 +1,32 @@ +import boa +from tests.utils.deployers import ( + CONSTANTS_DEPLOYER, + CONTROLLER_DEPLOYER, + LENDING_FACTORY_DEPLOYER, + VAULT_DEPLOYER, +) + +from typing import Final + +ZERO_ADDRESS: Final[str] = boa.eval("empty(address)") +MAX_UINT256: Final[int] = boa.eval("max_value(uint256)") +MAX_INT256: Final[int] = boa.eval("max_value(int256)") + +# Constants from contracts/constants.vy +WAD = CONSTANTS_DEPLOYER._constants.WAD +SWAD = CONSTANTS_DEPLOYER._constants.SWAD +DEAD_SHARES = CONSTANTS_DEPLOYER._constants.DEAD_SHARES +__version__ = CONSTANTS_DEPLOYER._constants.__version__ + +MAX_ORACLE_PRICE_DEVIATION = CONTROLLER_DEPLOYER._constants.MAX_ORACLE_PRICE_DEVIATION +MIN_TICKS = CONTROLLER_DEPLOYER._constants.MIN_TICKS +MAX_TICKS = CONTROLLER_DEPLOYER._constants.MAX_TICKS + +MIN_A = LENDING_FACTORY_DEPLOYER._constants.MIN_A +MAX_A = LENDING_FACTORY_DEPLOYER._constants.MAX_A +MIN_FEE = LENDING_FACTORY_DEPLOYER._constants.MIN_FEE +MAX_FEE = LENDING_FACTORY_DEPLOYER._constants.MAX_FEE +MAX_LOAN_DISCOUNT = LENDING_FACTORY_DEPLOYER._constants.MAX_LOAN_DISCOUNT +MIN_LIQUIDATION_DISCOUNT = LENDING_FACTORY_DEPLOYER._constants.MIN_LIQUIDATION_DISCOUNT + +MIN_ASSETS = VAULT_DEPLOYER._constants.MIN_ASSETS diff --git a/tests/utils/deployers.py b/tests/utils/deployers.py new file mode 100644 index 00000000..257a1719 --- /dev/null +++ b/tests/utils/deployers.py @@ -0,0 +1,283 @@ +""" +Centralized deployers for all contracts used in tests. +Each deployer is a VyperDeployer object returned using boa.load_partial(). +""" + +import boa +from vyper.compiler.settings import OptimizationLevel + +# Base compiler args +compiler_args_default = {"experimental_codegen": False} + +# Compiler args for different optimization levels +# Contracts with #pragma optimize codesize +compiler_args_codesize = { + **compiler_args_default, + "optimize": OptimizationLevel.CODESIZE, +} + +# Contracts with #pragma optimize gas +compiler_args_gas = {**compiler_args_default, "optimize": OptimizationLevel.GAS} + +# Contract paths +BASE_CONTRACT_PATH = "contracts/" +TESTING_CONTRACT_PATH = "contracts/testing/" +LENDING_CONTRACT_PATH = "contracts/lending/" +MPOLICIES_CONTRACT_PATH = "contracts/mpolicies/" +PRICE_ORACLES_CONTRACT_PATH = "contracts/price_oracles/" +STABILIZER_CONTRACT_PATH = "contracts/stabilizer/" +FLASHLOAN_CONTRACT_PATH = "contracts/flashloan/" +STABLESWAP_NG_PATH = "contracts/testing/stableswap-ng/contracts/main/" +ZAPS_CONTRACT_PATH = "contracts/zaps/" + +# Constants contract (for accessing constants) +CONSTANTS_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "constants.vy", compiler_args=compiler_args_default +) + +# Core contracts +AMM_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "AMM.vy", compiler_args=compiler_args_default +) +CONTROLLER_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "Controller.vy", compiler_args=compiler_args_codesize +) +CONTROLLER_VIEW_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "ControllerView.vy", compiler_args=compiler_args_codesize +) +MINT_CONTROLLER_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "MintController.vy", compiler_args=compiler_args_codesize +) +CONTROLLER_FACTORY_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "ControllerFactory.vy", compiler_args=compiler_args_default +) +STABLECOIN_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "Stablecoin.vy", compiler_args=compiler_args_default +) +STABLESWAP_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "Stableswap.vy", compiler_args=compiler_args_default +) + +# Lending contracts - all have #pragma optimize codesize +VAULT_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "Vault.vy", compiler_args=compiler_args_codesize +) +LEND_CONTROLLER_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LendController.vy", compiler_args=compiler_args_codesize +) +LEND_CONTROLLER_VIEW_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LendControllerView.vy", compiler_args=compiler_args_default +) +LENDING_FACTORY_DEPLOYER = boa.load_partial( + LENDING_CONTRACT_PATH + "LendingFactory.vy", compiler_args=compiler_args_codesize +) + +# Flashloan contracts +FLASH_LENDER_DEPLOYER = boa.load_partial( + FLASHLOAN_CONTRACT_PATH + "FlashLender.vy", compiler_args=compiler_args_default +) + +# Zap contracts +PARTIAL_REPAY_ZAP_DEPLOYER = boa.load_partial( + ZAPS_CONTRACT_PATH + "PartialRepayZap.vy", compiler_args=compiler_args_default +) +PARTIAL_REPAY_ZAP_CALLBACK_DEPLOYER = boa.load_partial( + ZAPS_CONTRACT_PATH + "PartialRepayZapCallback.vy", + compiler_args=compiler_args_default, +) + +# Monetary policies - all have no pragma +CONSTANT_MONETARY_POLICY_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "ConstantMonetaryPolicy.vy", + compiler_args=compiler_args_default, +) +CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "ConstantMonetaryPolicyLending.vy", + compiler_args=compiler_args_default, +) +SEMILOG_MONETARY_POLICY_DEPLOYER = boa.load_partial( + MPOLICIES_CONTRACT_PATH + "SemilogMonetaryPolicy.vy", + compiler_args=compiler_args_default, +) +SECONDARY_MONETARY_POLICY_DEPLOYER = boa.load_partial( + MPOLICIES_CONTRACT_PATH + "SecondaryMonetaryPolicy.vy", + compiler_args=compiler_args_default, +) +AGG_MONETARY_POLICY2_DEPLOYER = boa.load_partial( + MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy2.vy", + compiler_args=compiler_args_default, +) +AGG_MONETARY_POLICY3_DEPLOYER = boa.load_partial( + MPOLICIES_CONTRACT_PATH + "AggMonetaryPolicy3.vy", + compiler_args=compiler_args_default, +) + +# Price oracles +DUMMY_PRICE_ORACLE_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "DummyPriceOracle.vy", compiler_args=compiler_args_default +) +CRYPTO_FROM_POOL_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "CryptoFromPool.vy", + compiler_args=compiler_args_default, +) +EMA_PRICE_ORACLE_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "EmaPriceOracle.vy", + compiler_args=compiler_args_default, +) +AGGREGATE_STABLE_PRICE3_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "AggregateStablePrice3.vy", + compiler_args=compiler_args_default, +) +CRYPTO_WITH_STABLE_PRICE_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePrice.vy", + compiler_args=compiler_args_default, +) +CRYPTO_WITH_STABLE_PRICE_AND_CHAINLINK_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "CryptoWithStablePriceAndChainlink.vy", + compiler_args=compiler_args_default, +) + +# Proxy oracle contracts - have #pragma optimize gas +PROXY_ORACLE_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracle.vy", + compiler_args=compiler_args_gas, +) +PROXY_ORACLE_FACTORY_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "proxy/ProxyOracleFactory.vy", + compiler_args=compiler_args_gas, +) + +# LP oracle contracts +LP_ORACLE_STABLE_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleStable.vy", + compiler_args=compiler_args_default, +) +LP_ORACLE_CRYPTO_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleCrypto.vy", + compiler_args=compiler_args_default, +) +# LPOracleFactory.vy has #pragma optimize gas +LP_ORACLE_FACTORY_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/LPOracleFactory.vy", + compiler_args=compiler_args_gas, +) + +# Stabilizer contracts +PEG_KEEPER_V2_DEPLOYER = boa.load_partial( + STABILIZER_CONTRACT_PATH + "PegKeeperV2.vy", compiler_args=compiler_args_default +) +PEG_KEEPER_REGULATOR_DEPLOYER = boa.load_partial( + STABILIZER_CONTRACT_PATH + "PegKeeperRegulator.vy", + compiler_args=compiler_args_default, +) +PEG_KEEPER_OFFBOARDING_DEPLOYER = boa.load_partial( + STABILIZER_CONTRACT_PATH + "PegKeeperOffboarding.vy", + compiler_args=compiler_args_default, +) + +# Callback contracts +LM_CALLBACK_DEPLOYER = boa.load_partial( + BASE_CONTRACT_PATH + "LMCallback.vy", compiler_args=compiler_args_default +) + +# Testing/Mock contracts +ERC20_MOCK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "ERC20Mock.vy", compiler_args=compiler_args_default +) +ERC20_CRV_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "ERC20CRV.vy", compiler_args=compiler_args_default +) +WETH_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "WETH.vy", compiler_args=compiler_args_default +) +VOTING_ESCROW_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "VotingEscrow.vy", compiler_args=compiler_args_default +) +GAUGE_CONTROLLER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "GaugeController.vy", compiler_args=compiler_args_default +) +MINTER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "Minter.vy", compiler_args=compiler_args_default +) +FAKE_LEVERAGE_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "FakeLeverage.vy", compiler_args=compiler_args_default +) +DUMMY_CALLBACK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "DummyCallback.vy", compiler_args=compiler_args_default +) +BLOCK_COUNTER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "BlockCounter.vy", compiler_args=compiler_args_default +) +DUMMY_FLASH_BORROWER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "DummyFlashBorrower.vy", compiler_args=compiler_args_default +) +DUMMY_LM_CALLBACK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "DummyLMCallback.vy", compiler_args=compiler_args_default +) +LM_CALLBACK_WITH_REVERTS_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "LMCallbackWithReverts.vy", + compiler_args=compiler_args_default, +) +MOCK_FACTORY_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockFactory.vy", compiler_args=compiler_args_default +) +MOCK_MARKET_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockMarket.vy", compiler_args=compiler_args_default +) +MOCK_RATE_SETTER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockRateSetter.vy", compiler_args=compiler_args_default +) +MOCK_PEG_KEEPER_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockPegKeeper.vy", compiler_args=compiler_args_default +) +MOCK_RATE_ORACLE_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockRateOracle.vy", compiler_args=compiler_args_default +) +CHAINLINK_AGGREGATOR_MOCK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "ChainlinkAggregatorMock.vy", + compiler_args=compiler_args_default, +) +TRICRYPTO_MOCK_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "TricryptoMock.vy", compiler_args=compiler_args_default +) +MOCK_SWAP2_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockSwap2.vy", compiler_args=compiler_args_default +) +MOCK_SWAP3_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "MockSwap3.vy", compiler_args=compiler_args_default +) +SWAP_FACTORY_DEPLOYER = boa.load_partial( + TESTING_CONTRACT_PATH + "SwapFactory.vy", compiler_args=compiler_args_default +) + +# LP oracle testing contracts +MOCK_STABLE_SWAP_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockStableSwap.vy", + compiler_args=compiler_args_default, +) +MOCK_CRYPTO_SWAP_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockCryptoSwap.vy", + compiler_args=compiler_args_default, +) +MOCK_STABLE_SWAP_NO_ARGUMENT_DEPLOYER = boa.load_partial( + PRICE_ORACLES_CONTRACT_PATH + "lp-oracles/testing/MockStableSwapNoArgument.vy", + compiler_args=compiler_args_default, +) + +# Stableswap NG contracts +CURVE_STABLESWAP_FACTORY_NG_DEPLOYER = boa.load_partial( + STABLESWAP_NG_PATH + "CurveStableSwapFactoryNG.vy", + compiler_args=compiler_args_default, +) +# CurveStableSwapNG.vy has #pragma optimize codesize +CURVE_STABLESWAP_NG_DEPLOYER = boa.load_partial( + STABLESWAP_NG_PATH + "CurveStableSwapNG.vy", compiler_args=compiler_args_codesize +) +# CurveStableSwapNGMath.vy has #pragma optimize gas +CURVE_STABLESWAP_NG_MATH_DEPLOYER = boa.load_partial( + STABLESWAP_NG_PATH + "CurveStableSwapNGMath.vy", compiler_args=compiler_args_gas +) +CURVE_STABLESWAP_NG_VIEWS_DEPLOYER = boa.load_partial( + STABLESWAP_NG_PATH + "CurveStableSwapNGViews.vy", + compiler_args=compiler_args_default, +) diff --git a/tests/utils/protocols.py b/tests/utils/protocols.py new file mode 100644 index 00000000..3d3aae9a --- /dev/null +++ b/tests/utils/protocols.py @@ -0,0 +1,310 @@ +""" +Deploy function for the complete llamalend protocol suite. +Provides deployment of both mint and lending markets with all necessary contracts. +""" + +import boa +from boa.contracts.vyper.vyper_contract import ( + VyperDeployer, + VyperBlueprint, + VyperContract, +) +from typing import Dict, Any + +from tests.utils.constants import MAX_UINT256 + + +from tests.utils.deployers import ( + # Core contracts + STABLECOIN_DEPLOYER, + AMM_DEPLOYER, + CONTROLLER_FACTORY_DEPLOYER, + CONTROLLER_VIEW_DEPLOYER, + # Lending contracts + VAULT_DEPLOYER, + LEND_CONTROLLER_DEPLOYER, + LEND_CONTROLLER_VIEW_DEPLOYER, + LENDING_FACTORY_DEPLOYER, + # Price oracles + DUMMY_PRICE_ORACLE_DEPLOYER, + CRYPTO_FROM_POOL_DEPLOYER, + # Monetary policies + CONSTANT_MONETARY_POLICY_DEPLOYER, + CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + SEMILOG_MONETARY_POLICY_DEPLOYER, + WETH_DEPLOYER, + ERC20_MOCK_DEPLOYER, + # Compiler flags + compiler_args_codesize, +) + + +class Blueprints: + def __init__(self, **deployers: VyperDeployer): + for name, deployer in deployers.items(): + setattr(self, name, deployer.deploy_as_blueprint()) + + amm: VyperBlueprint + mint_controller: VyperBlueprint + lend_controller: VyperBlueprint + lend_controller_view: VyperBlueprint + price_oracle: VyperBlueprint + mpolicy: VyperBlueprint + vault: VyperBlueprint + + +class Llamalend: + """ + Protocol deployment and management class for llamalend. + Handles deployment of core infrastructure and creation of markets. + """ + + def __init__(self, initial_price: int = 3000 * 10**18): + """ + Deploy the complete llamalend protocol suite. + + Args: + admin: Admin address for all contracts + initial_price: Initial price for oracles (e.g., 3000 * 10**18) + """ + self.admin = boa.env.generate_address("admin") + self.fee_receiver = boa.env.generate_address("fee_receiver") + + controller_view_blueprint = CONTROLLER_VIEW_DEPLOYER.deploy_as_blueprint() + + with open("contracts/MintController.vy", "r") as f: + mint_controller_code = f.read() + mint_controller_code = mint_controller_code.replace( + "empty(address), # to replace at deployment with view blueprint", + f"{controller_view_blueprint.address},", + 1, + ) + assert f"{controller_view_blueprint.address}," in mint_controller_code + + # This is a bit of a special case that breaks our current conventions around + # deployers. The only reason why this was done is because since we can't change + # the constructor arguments of the MintController contract, we have to + # manually patch the code to insert the correct address of the controller view + # which is only known at runtime. + self._mint_controller_deployer = boa.loads_partial( + mint_controller_code, compiler_args=compiler_args_codesize + ) + + # Deploy all blueprints + self.blueprints = Blueprints( + amm=AMM_DEPLOYER, + mint_controller=self._mint_controller_deployer, + lend_controller=LEND_CONTROLLER_DEPLOYER, + lend_controller_view=LEND_CONTROLLER_VIEW_DEPLOYER, + price_oracle=CRYPTO_FROM_POOL_DEPLOYER, + mpolicy=CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER, + vault=VAULT_DEPLOYER, + ) + + # Deploy core infrastructure + with boa.env.prank(self.admin): + # Deploy stablecoin + self.crvUSD = STABLECOIN_DEPLOYER.deploy("Curve USD", "crvUSD") + self.__init_mint_markets(initial_price) + self.__init_lend_markets() + + def __init_mint_markets(self, initial_price): + # Deploy WETH + self.weth = WETH_DEPLOYER.deploy() + + # Deploy a dummy price oracle for testing + self.price_oracle = DUMMY_PRICE_ORACLE_DEPLOYER.deploy( + self.admin, initial_price + ) + + # Deploy Mint Protocol + # Deploy controller factory + self.mint_factory = CONTROLLER_FACTORY_DEPLOYER.deploy( + self.crvUSD.address, self.admin, self.fee_receiver, self.weth.address + ) + + # Set implementations on factory using blueprints + self.mint_factory.set_implementations( + self.blueprints.mint_controller.address, self.blueprints.amm.address + ) + + # Set stablecoin minter to factory + self.crvUSD.set_minter(self.mint_factory.address) + + # Deploy monetary policy for mint markets + self.mint_monetary_policy = CONSTANT_MONETARY_POLICY_DEPLOYER.deploy(self.admin) + + def __init_lend_markets(self): + # Deploy Lending Protocol + # Deploy lending factory + self.lending_factory = LENDING_FACTORY_DEPLOYER.deploy( + self.blueprints.amm.address, + self.blueprints.lend_controller.address, + self.blueprints.vault.address, + self.blueprints.price_oracle.address, + self.blueprints.lend_controller_view.address, + self.admin, + self.fee_receiver, + ) + + def create_mint_market( + self, + collateral_token: VyperContract, + price_oracle: VyperContract, + monetary_policy: VyperContract, + A: int, + amm_fee: int, + loan_discount: int, + liquidation_discount: int, + debt_ceiling: int, + ) -> Dict[str, VyperContract]: + """ + Create a new mint market in the Controller Factory. + + Args: + collateral_token: Collateral token contract + price_oracle: Price oracle contract + monetary_policy: Monetary policy contract for this market + A: AMM amplification parameter (e.g., 100) + fee: Trading fee (e.g., 10**16 for 1%) + loan_discount: Loan discount (e.g., 9 * 10**16 for 9%) + liquidation_discount: Liquidation discount (e.g., 6 * 10**16 for 6%) + debt_ceiling: Maximum debt for this market (e.g., 10**6 * 10**18) + + Returns: + Dictionary with 'controller' and 'amm' contracts + """ + self.mint_factory.add_market( + collateral_token.address, + A, + amm_fee, + 0, # admin fee deprecated for mint markets + price_oracle.address, + monetary_policy.address, + loan_discount, + liquidation_discount, + debt_ceiling, + sender=self.admin, + ) + + controller_address = self.mint_factory.get_controller(collateral_token.address) + amm_address = self.mint_factory.get_amm(collateral_token.address) + + return { + "controller": self._mint_controller_deployer.at(controller_address), + "amm": AMM_DEPLOYER.at(amm_address), + } + + def create_lending_market( + self, + borrowed_token: VyperContract, + collateral_token: Any, + A: int, + fee: int, + loan_discount: int, + liquidation_discount: int, + price_oracle: VyperContract, + name: str, + min_borrow_rate: int, + max_borrow_rate: int, + seed_amount: int = 1000 * 10**18, + mpolicy_deployer: VyperDeployer | None = None, + supply_limit: int = MAX_UINT256, + ) -> Dict[str, VyperContract]: + """ + Create a new lending market in the Lending Factory. + + Args: + borrowed_token: Token to be borrowed + collateral_token: Token used as collateral + A: AMM amplification parameter (e.g., 100) + fee: Trading fee (e.g., 6 * 10**15 for 0.6%) + loan_discount: Loan discount (e.g., 9 * 10**16 for 9%) + liquidation_discount: Liquidation discount (e.g., 6 * 10**16 for 6%) + price_oracle: Price oracle contract + name: Name for the vault + min_borrow_rate: Minimum borrow rate (e.g., 0.5 * 10**16 for 0.5%) + max_borrow_rate: Maximum borrow rate (e.g., 50 * 10**16 for 50%) + seed_amount: Borrowed token amount to seed into the vault post-deployment + mpolicy_deployer: Optional monetary policy deployer override + supply_limit: Vault supply cap passed to the factory (defaults to MAX_UINT256 for no cap) + + Returns: + Dictionary with 'vault', 'controller', 'amm' contracts. + """ + policy_deployer = mpolicy_deployer or CONSTANT_MONETARY_POLICY_LENDING_DEPLOYER + + deploy_args = [ + borrowed_token.address, + min_borrow_rate, + max_borrow_rate, + ] + if policy_deployer is SEMILOG_MONETARY_POLICY_DEPLOYER: + deploy_args.append(self.lending_factory.address) + + monetary_policy = policy_deployer.deploy(*deploy_args) + + result = self.lending_factory.create( + borrowed_token.address, + collateral_token.address, + A, + fee, + loan_discount, + liquidation_discount, + price_oracle.address, + monetary_policy.address, + name, + supply_limit, + sender=self.admin, + ) + + vault = VAULT_DEPLOYER.at(result[0]) + controller = LEND_CONTROLLER_DEPLOYER.at(result[1]) + amm = AMM_DEPLOYER.at(result[2]) + + # Seed lending markets by depositing borrowed token into the vault + if seed_amount and seed_amount > 0: + with boa.env.prank(self.admin): + boa.deal(borrowed_token, self.admin, seed_amount) + borrowed_token.approve(vault.address, 2**256 - 1) + vault.deposit(seed_amount) + + return { + "vault": vault, + "controller": controller, + "amm": amm, + } + + +if __name__ == "__main__": + proto = Protocol() + + # Test mint market creation + collat = ERC20_MOCK_DEPLOYER.deploy(18) + mint_market = proto.create_mint_market( + collat, + proto.price_oracle, + proto.mint_monetary_policy, + A=100, + amm_fee=10**16, + loan_discount=9 * 10**16, # 9% + liquidation_discount=6 * 10**16, # 6% + debt_ceiling=10**6 * 10**18, + ) + + # Test lending market creation + borrowed_token = ERC20_MOCK_DEPLOYER.deploy(18) + collat_token = ERC20_MOCK_DEPLOYER.deploy(18) + + lending_market = proto.create_lending_market( + borrowed_token=borrowed_token, + collateral_token=collat_token, + A=100, + fee=6 * 10**15, # 0.6% + loan_discount=9 * 10**16, # 9% + liquidation_discount=6 * 10**16, # 6% + price_oracle=proto.price_oracle, + name="Test Vault", + min_borrow_rate=5 * 10**15 // (365 * 86400), # 0.5% APR + max_borrow_rate=50 * 10**16 // (365 * 86400), # 50% APR + ) diff --git a/tests/zaps/__init__.py b/tests/zaps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/zaps/conftest.py b/tests/zaps/conftest.py new file mode 100644 index 00000000..f2791e1a --- /dev/null +++ b/tests/zaps/conftest.py @@ -0,0 +1,12 @@ +import pytest + + +@pytest.fixture(scope="module") +def amm_A(): + return 100 + + +@pytest.fixture(scope="module") +def seed_liquidity(): + # Match the seeding used by these tests (assumes 18 decimals) + return 100 * 10**6 * 10**18 diff --git a/tests/zaps/partial_liquidation/__init__.py b/tests/zaps/partial_liquidation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/zaps/partial_liquidation/calculations/Partial_Liquidation_Zap_Evaluation.pdf b/tests/zaps/partial_liquidation/calculations/Partial_Liquidation_Zap_Evaluation.pdf new file mode 100644 index 00000000..db6d0a4b Binary files /dev/null and b/tests/zaps/partial_liquidation/calculations/Partial_Liquidation_Zap_Evaluation.pdf differ diff --git a/tests/zaps/partial_liquidation/calculations/calculations.py b/tests/zaps/partial_liquidation/calculations/calculations.py new file mode 100644 index 00000000..50a65174 --- /dev/null +++ b/tests/zaps/partial_liquidation/calculations/calculations.py @@ -0,0 +1,599 @@ +import os +import boa +import logging +import requests + +from web3 import HTTPProvider, Web3 +import json + +logger = logging.getLogger("warnings") +logger.setLevel(logging.ERROR) + +positions = [ + [ + 21758686, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x81e3b8957e9Ab1b3ae1785Fd6ba7B1AcC2173490", + ], + [ + 20542247, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x00Fb4599c681CA15FDF1AA537B066630876102aE", + ], + [ + 20242554, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x0477011BBb0BD4a6A8a59182FF58dE328E3592a4", + ], + [ + 20267600, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x10e50901A9F65629715db45B24e5b047a6F11240", + ], + [ + 20665126, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x1C79cb8Ce8C3695ed871E4D4e4519D937630832d", + ], + [ + 18818673, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x1CB82662c90260A6Cbe6Ab0B8298a3208B666b91", + ], + [ + 20563722, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x1d04Cd3BC5C9EDA103D7Feb2DB72350dB612E99a", + ], + [ + 17939156, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x1E268f0802321078A20153EcFA7Be65e4a078C59", + ], + [ + 19570343, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x1e42E843e666Fc90844e609Bd66c32E2A2A0d5E8", + ], + [ + 21741988, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x201051Ae0FddaC0Ce47B299E4673cAA39f32A961", + ], + [ + 19956294, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x2098ed20eD0d7a78023977dDcd33DD8c596D1d03", + ], + [ + 21725260, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x20b9Efc68DB884Db2a6f804265f2F6ba611a0F41", + ], + [ + 20606655, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x229A72082D79a6FC5EE65BCC9147a5B8501C9016", + ], + [ + 21078316, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x23bF6fd9c3Cb027fACd71d052981C3F74E1f1860", + ], + [ + 19482899, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x26701C848abf9f2422A1FD5aA9B8b4544328A043", + ], + [ + 20480142, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x29D37036C9EE7e76413b93BEEe9e2c1b8E8Ad368", + ], + [ + 19650067, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x2F0126966657563C4a376cE43ba43891d7537A32", + ], + [ + 21768935, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x314e5699db4756138107AE7d7EeDDf5708583ff5", + ], + [ + 18871481, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x3182696B6B106a68a3062a8798183D87f20Ed598", + ], + [ + 21498433, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x332192943b3E347c5733924995d3423F26186b5c", + ], + [ + 19098969, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x369F54c03447B0f3af7760CE730a1364D1c23e1E", + ], + [ + 20223450, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x442ABE3cef7665cEC0E57715EE8BA686762865ae", + ], + [ + 21214455, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x4BD46cd07Dbc52805B424CbED5942A0F24E28725", + ], + [ + 20159005, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x4Cb43773C56a85991f3201A316C650cA88acC873", + ], + [ + 20480142, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x4cC7303602376679a0E69434f3B25703e465F535", + ], + [ + 18355636, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x4da79bBf1f9cB0086684E8Eb0fb5e559952Bd0BC", + ], + [ + 20694185, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x547E826F342f0355055E9424D5797D5Bc5F73221", + ], + [ + 20552973, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x5A684c08261380B91D8976eDB0cabf87744650a5", + ], + [ + 19113233, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x5e52b108018Dd567A34b0B403b95cBA848ab974C", + ], + [ + 19500711, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x5Eae7CDc3A9F357B1Ca1f4918dB664A9E7CD5FF6", + ], + [ + 17940349, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x5fF910c34Db2Ae9Eeee29cf6902197CBC7B6812D", + ], + [ + 20111365, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x60Fa6eEf5415aD3a7FbEC63F3419eb5F590b88cb", + ], + [ + 18311584, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x6119Fa6C5B18BE03F3b8E408c961E28239A0108C", + ], + [ + 19371711, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x63Fe1742192f07D67739cc0f091645a2A50804E1", + ], + [ + 18962158, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x654adAa768FD13c3904fD64B56E1d2A530447D47", + ], + [ + 20625752, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x65FacE9427f10a7818698de0343201c8e494aCFD", + ], + [ + 20768982, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x6690547dA5fcB5d775f18d4473Cd9c05eBFFE545", + ], + [ + 18381864, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x6764E71d06f5947784B81718A834afFaf548b5cB", + ], + [ + 20285517, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x6943A928EE855BbE7A7F96Dab4178Dfda3fB91e0", + ], + [ + 20074413, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x6D1F9CF37Cfb93a2eC0125bA107a251F459cc575", + ], + [ + 20472964, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x7021528C73E008E06E7D83a1e0697D0b072F0D0B", + ], + [ + 18302049, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x7944642920Df33BAE461f86Aa0cd0b4B8284330E", + ], + [ + 20462218, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x794F2331F69f9D276a3A006953669CD2FC23Ab92", + ], + [ + 19754870, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x79A47EA2bF6C0f036E8EB1022a7693e2cffD5C50", + ], + [ + 19703638, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x7BaE493fb2f56F43cdc535d6Ad6C845f8C2B35e2", + ], + [ + 21031734, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x81e3b8957e9Ab1b3ae1785Fd6ba7B1AcC2173490", + ], + [ + 19641764, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x83786E8634813DbF45E305bb28B7fCf855D314A7", + ], + [ + 17736621, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x83A6851f146A272Ff257afd7509f9747A87FB689", + ], + [ + 18136740, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x84834D3B6844E25CE6911a50897EC073Fb489568", + ], + [ + 18087987, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x8a625BF21c01A83d93D1175556Ef3aF76b862c83", + ], + [ + 20403708, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x8d46f6E3B726D17139238d8d1d372838Be422dBE", + ], + [ + 20692603, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x8E53B23D255208CBF2Ff86Eb282f30CEB61539ED", + ], + [ + 20710519, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x8ec6A1e5Af3656b78f99D21687422E237Df6e384", + ], + [ + 20128026, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x8Fee6B44B975D9BF99728Ae22E1FEDEc38De2Bcf", + ], + [ + 20816722, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x92957DC2Fc5c1E40b117EB1f9515acb601d6A1f1", + ], + [ + 19476963, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x93285233955DdD615f1bAEaa7825D662152c6F24", + ], + [ + 18399734, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x944cF2acB0DB10d0863fE45C4916B2fd7005C6a4", + ], + [ + 20699769, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0x9C398b892B492787E79FB998078b56Ba0F6A250B", + ], + [ + 21650034, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xA2471055588395Dc8A88614dA1CeE0Ce2512f85F", + ], + [ + 20287898, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xA4B738b8E5dbb479FdB7489958F9dD5569FbbCEa", + ], + [ + 20456791, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xa8c76bE1297B81b0CE3874D7E8B4a44F7d1f7E0b", + ], + [ + 18296083, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xAa9E20bAb58d013220D632874e9Fe44F8F971e4d", + ], + [ + 19439013, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xAb40e16d0156D14169D0feF043B3f2FcC6A43fD0", + ], + [ + 21200117, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xAD60032Bb3fB14b7d863877B5C2FB9833913919C", + ], + [ + 19641764, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xB221963CAD5856c657647D7126A6Fe6A47CaC773", + ], + [ + 21379122, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xb29E391A1124Ab6c6e68A210cdFC5824c8E2A4B5", + ], + [ + 20748706, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xb3268b55dB5D25A88BAfFa261977c1F1C8e989b8", + ], + [ + 20458453, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xb820Fd41dbFEd079Ea2F612399361E5033Dd7af7", + ], + [ + 19472215, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xBaF17D8126eFB243F56B9cF814aBAB6B5d34AE37", + ], + [ + 18147390, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xBbF31ae642CceB471380D64D987f925aFcF6C32a", + ], + [ + 18796120, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xC3b876976D58dD9c2e8ab8ce0446C1D5eD8bF55c", + ], + [ + 18844809, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xc3C6B0C4F9C3871b072DC087336A5391f9BF3c07", + ], + [ + 20112551, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xC4aFfC415D30b7518c724114F6374172F97F4C0f", + ], + [ + 18621189, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xC773B66E3766FF7515bbA8906f99E4BfBA958D65", + ], + [ + 21472162, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xc99989c2913BF8Cf1978E6fd7fcDb587a4D3fd3b", + ], + [ + 18392585, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xCD67353EcC3755C1f4f3976a1e1929fB5e61aa33", + ], + [ + 19683394, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xce6C68Bf0567F14a0FB43D85B707d5EaFdA8027A", + ], + [ + 21017392, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xCf115cBA7638FFDe4d32E9fD8d0A70d131b42717", + ], + [ + 19104897, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xD3Dd68F794174cbadf0dA25fb15cdf9D4D673D45", + ], + [ + 18809168, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xd416B5510645b532E1414fa71F4aD895abDc4D44", + ], + [ + 18819857, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xd5d7d4bA0f5FBCC8c2c04D14EcE01dC6e6261DC0", + ], + [ + 18816304, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xD6AF98Abce0f9260Fcd2C1c49884413fcDC60F6F", + ], + [ + 20283113, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xd87EcC6C74F486B044824a222326A96F696fCfA2", + ], + [ + 18356829, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xD9F4DdA53ABc0ad6eae07eD2e47b3108c3a131b8", + ], + [ + 20991096, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xda139B194Fd979622DeA0381F6D206790B0D6F41", + ], + [ + 18215044, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xE408D65d495c567aB246E7c90F11d15d96c1738D", + ], + [ + 20467001, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xe4727db0D9eF3cA11b9D177c2E92f63b512993A5", + ], + [ + 20461028, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xe77Adf593302A6524D054A27E886021c2cEf8c0B", + ], + [ + 19491216, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xE9e2aa87f02e92c33b7A4C705196c9218b11d2e5", + ], + [ + 19444955, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xea3F9b017C6b811B0a8Ca642346Bd805D936Fce4", + ], + [ + 20055315, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xebdeff4D7053bF84262D2f9FC261a900c4323d83", + ], + [ + 19889538, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xEC3c1940d88B8875b39b41e6D023026da500D4bB", + ], + [ + 18817492, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xf11F0Add0e8E4ee208104d8264fcf1B69C4CeAfc", + ], + [ + 18204326, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xF9605D8c4c987d7Cb32D0d11FbCb8EeeB1B22D5d", + ], + [ + 20827471, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xfa8b48009A1749442566882B814927B239bE131F", + ], + [ + 20480137, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xfBB4D0C7282E3cfB1F1243345F245188b17cC2Fd", + ], + [ + 21379079, + "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635", + "0xfea6e4c830210361aBA38A07828B73686643f411", + ], +] + +url = os.getenv("WEB3_PROVIDER_URL") + +abi = """[{"name":"UserState","inputs":[{"name":"user","type":"address","indexed":true},{"name":"collateral","type":"uint256","indexed":false},{"name":"debt","type":"uint256","indexed":false},{"name":"n1","type":"int256","indexed":false},{"name":"n2","type":"int256","indexed":false},{"name":"liquidation_discount","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Borrow","inputs":[{"name":"user","type":"address","indexed":true},{"name":"collateral_increase","type":"uint256","indexed":false},{"name":"loan_increase","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Repay","inputs":[{"name":"user","type":"address","indexed":true},{"name":"collateral_decrease","type":"uint256","indexed":false},{"name":"loan_decrease","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveCollateral","inputs":[{"name":"user","type":"address","indexed":true},{"name":"collateral_decrease","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Liquidate","inputs":[{"name":"liquidator","type":"address","indexed":true},{"name":"user","type":"address","indexed":true},{"name":"collateral_received","type":"uint256","indexed":false},{"name":"stablecoin_received","type":"uint256","indexed":false},{"name":"debt","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetMonetaryPolicy","inputs":[{"name":"monetary_policy","type":"address","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetBorrowingDiscounts","inputs":[{"name":"loan_discount","type":"uint256","indexed":false},{"name":"liquidation_discount","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"CollectFees","inputs":[{"name":"amount","type":"uint256","indexed":false},{"name":"new_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"collateral_token","type":"address"},{"name":"monetary_policy","type":"address"},{"name":"loan_discount","type":"uint256"},{"name":"liquidation_discount","type":"uint256"},{"name":"amm","type":"address"}],"outputs":[]},{"stateMutability":"payable","type":"fallback"},{"stateMutability":"view","type":"function","name":"factory","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"amm","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"collateral_token","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"debt","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"loan_exists","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"total_debt","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"max_borrowable","inputs":[{"name":"collateral","type":"uint256"},{"name":"N","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"max_borrowable","inputs":[{"name":"collateral","type":"uint256"},{"name":"N","type":"uint256"},{"name":"current_debt","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"min_collateral","inputs":[{"name":"debt","type":"uint256"},{"name":"N","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"calculate_debt_n1","inputs":[{"name":"collateral","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"N","type":"uint256"}],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"payable","type":"function","name":"create_loan","inputs":[{"name":"collateral","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"N","type":"uint256"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"create_loan_extended","inputs":[{"name":"collateral","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"N","type":"uint256"},{"name":"callbacker","type":"address"},{"name":"callback_args","type":"uint256[]"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"add_collateral","inputs":[{"name":"collateral","type":"uint256"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"add_collateral","inputs":[{"name":"collateral","type":"uint256"},{"name":"_for","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_collateral","inputs":[{"name":"collateral","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_collateral","inputs":[{"name":"collateral","type":"uint256"},{"name":"use_eth","type":"bool"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"borrow_more","inputs":[{"name":"collateral","type":"uint256"},{"name":"debt","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"repay","inputs":[{"name":"_d_debt","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"repay","inputs":[{"name":"_d_debt","type":"uint256"},{"name":"_for","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"repay","inputs":[{"name":"_d_debt","type":"uint256"},{"name":"_for","type":"address"},{"name":"max_active_band","type":"int256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"repay","inputs":[{"name":"_d_debt","type":"uint256"},{"name":"_for","type":"address"},{"name":"max_active_band","type":"int256"},{"name":"use_eth","type":"bool"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"repay_extended","inputs":[{"name":"callbacker","type":"address"},{"name":"callback_args","type":"uint256[]"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"health_calculator","inputs":[{"name":"user","type":"address"},{"name":"d_collateral","type":"int256"},{"name":"d_debt","type":"int256"},{"name":"full","type":"bool"}],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"health_calculator","inputs":[{"name":"user","type":"address"},{"name":"d_collateral","type":"int256"},{"name":"d_debt","type":"int256"},{"name":"full","type":"bool"},{"name":"N","type":"uint256"}],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"nonpayable","type":"function","name":"liquidate","inputs":[{"name":"user","type":"address"},{"name":"min_x","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"liquidate","inputs":[{"name":"user","type":"address"},{"name":"min_x","type":"uint256"},{"name":"use_eth","type":"bool"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"liquidate_extended","inputs":[{"name":"user","type":"address"},{"name":"min_x","type":"uint256"},{"name":"frac","type":"uint256"},{"name":"use_eth","type":"bool"},{"name":"callbacker","type":"address"},{"name":"callback_args","type":"uint256[]"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"tokens_to_liquidate","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"tokens_to_liquidate","inputs":[{"name":"user","type":"address"},{"name":"frac","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"health","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"health","inputs":[{"name":"user","type":"address"},{"name":"full","type":"bool"}],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"users_to_liquidate","inputs":[],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"user","type":"address"},{"name":"x","type":"uint256"},{"name":"y","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"health","type":"int256"}]}]},{"stateMutability":"view","type":"function","name":"users_to_liquidate","inputs":[{"name":"_from","type":"uint256"}],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"user","type":"address"},{"name":"x","type":"uint256"},{"name":"y","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"health","type":"int256"}]}]},{"stateMutability":"view","type":"function","name":"users_to_liquidate","inputs":[{"name":"_from","type":"uint256"},{"name":"_limit","type":"uint256"}],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"user","type":"address"},{"name":"x","type":"uint256"},{"name":"y","type":"uint256"},{"name":"debt","type":"uint256"},{"name":"health","type":"int256"}]}]},{"stateMutability":"view","type":"function","name":"amm_price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"user_prices","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"view","type":"function","name":"user_state","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256[4]"}]},{"stateMutability":"nonpayable","type":"function","name":"set_amm_fee","inputs":[{"name":"fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_amm_admin_fee","inputs":[{"name":"fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_monetary_policy","inputs":[{"name":"monetary_policy","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_borrowing_discounts","inputs":[{"name":"loan_discount","type":"uint256"},{"name":"liquidation_discount","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_callback","inputs":[{"name":"cb","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"admin_fees","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"collect_fees","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"liquidation_discounts","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"loans","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"loan_ix","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"n_loans","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"minted","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"redeemed","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"monetary_policy","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"liquidation_discount","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"loan_discount","inputs":[],"outputs":[{"name":"","type":"uint256"}]}]""" +amm_abi = """[{"name":"TokenExchange","inputs":[{"name":"buyer","type":"address","indexed":true},{"name":"sold_id","type":"uint256","indexed":false},{"name":"tokens_sold","type":"uint256","indexed":false},{"name":"bought_id","type":"uint256","indexed":false},{"name":"tokens_bought","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Deposit","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"amount","type":"uint256","indexed":false},{"name":"n1","type":"int256","indexed":false},{"name":"n2","type":"int256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Withdraw","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"amount_borrowed","type":"uint256","indexed":false},{"name":"amount_collateral","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetRate","inputs":[{"name":"rate","type":"uint256","indexed":false},{"name":"rate_mul","type":"uint256","indexed":false},{"name":"time","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetFee","inputs":[{"name":"fee","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"SetAdminFee","inputs":[{"name":"fee","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"_borrowed_token","type":"address"},{"name":"_borrowed_precision","type":"uint256"},{"name":"_collateral_token","type":"address"},{"name":"_collateral_precision","type":"uint256"},{"name":"_A","type":"uint256"},{"name":"_sqrt_band_ratio","type":"uint256"},{"name":"_log_A_ratio","type":"int256"},{"name":"_base_price","type":"uint256"},{"name":"fee","type":"uint256"},{"name":"admin_fee","type":"uint256"},{"name":"_price_oracle_contract","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_admin","inputs":[{"name":"_admin","type":"address"}],"outputs":[]},{"stateMutability":"pure","type":"function","name":"coins","inputs":[{"name":"i","type":"uint256"}],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"price_oracle","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"dynamic_fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_rate_mul","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_base_price","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"p_current_up","inputs":[{"name":"n","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"p_current_down","inputs":[{"name":"n","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"p_oracle_up","inputs":[{"name":"n","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"p_oracle_down","inputs":[{"name":"n","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_p","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"read_user_tick_numbers","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"int256[2]"}]},{"stateMutability":"view","type":"function","name":"can_skip_bands","inputs":[{"name":"n_end","type":"int256"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"view","type":"function","name":"active_band_with_skip","inputs":[],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"has_liquidity","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"deposit_range","inputs":[{"name":"user","type":"address"},{"name":"amount","type":"uint256"},{"name":"n1","type":"int256"},{"name":"n2","type":"int256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"withdraw","inputs":[{"name":"user","type":"address"},{"name":"frac","type":"uint256"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"view","type":"function","name":"get_dy","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"in_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_dxdy","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"in_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_dx","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"out_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_dydx","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"out_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"exchange","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"in_amount","type":"uint256"},{"name":"min_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"nonpayable","type":"function","name":"exchange","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"in_amount","type":"uint256"},{"name":"min_amount","type":"uint256"},{"name":"_for","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"nonpayable","type":"function","name":"exchange_dy","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"out_amount","type":"uint256"},{"name":"max_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"nonpayable","type":"function","name":"exchange_dy","inputs":[{"name":"i","type":"uint256"},{"name":"j","type":"uint256"},{"name":"out_amount","type":"uint256"},{"name":"max_amount","type":"uint256"},{"name":"_for","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"view","type":"function","name":"get_y_up","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_x_down","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_sum_xy","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}]},{"stateMutability":"view","type":"function","name":"get_xy","inputs":[{"name":"user","type":"address"}],"outputs":[{"name":"","type":"uint256[][2]"}]},{"stateMutability":"view","type":"function","name":"get_amount_for_price","inputs":[{"name":"p","type":"uint256"}],"outputs":[{"name":"","type":"uint256"},{"name":"","type":"bool"}]},{"stateMutability":"nonpayable","type":"function","name":"set_rate","inputs":[{"name":"rate","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"set_fee","inputs":[{"name":"fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_admin_fee","inputs":[{"name":"fee","type":"uint256"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"reset_admin_fees","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_callback","inputs":[{"name":"liquidity_mining_callback","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"admin","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"A","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"admin_fee","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"rate","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"active_band","inputs":[],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"min_band","inputs":[],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"max_band","inputs":[],"outputs":[{"name":"","type":"int256"}]},{"stateMutability":"view","type":"function","name":"admin_fees_x","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"admin_fees_y","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"price_oracle_contract","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"bands_x","inputs":[{"name":"arg0","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"bands_y","inputs":[{"name":"arg0","type":"int256"}],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"liquidity_mining_callback","inputs":[],"outputs":[{"name":"","type":"address"}]}]""" + + +def test_position(position, frac): + block_number = position[0] + controller_address = position[1] + user = position[2] + + block = json.loads( + Web3.to_json(Web3(HTTPProvider(url)).eth.get_block(block_number)) + ) + + coin = "ethereum:0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + prices_url = f"https://coins.llama.fi/prices/historical/{block['timestamp']}/{coin}" + resp = requests.get(prices_url) + spot_price = resp.json()["coins"][coin]["price"] + + boa.fork(url, block_identifier=block_number, allow_dirty=True) + controller = boa.loads_abi(abi, name="Controller").at(controller_address) + amm = boa.loads_abi(amm_abi, name="AMM").at(controller.amm()) + + # collateral, stablecoin, debt, N + user_state = controller.user_state(user) + health = controller.health(user) + x_down = amm.get_x_down(user) + ratio = x_down / user_state[2] + + t = f"User: {user}, block: {block_number}, ratio: {ratio}" + t += "\n" + ( + f"user state: health: {health * 100 / 1e18}, collateral: {user_state[0] / 1e18}, " + f"stablecoin: {user_state[1] / 1e18}, debt: {user_state[2] / 1e18}, ratio: {ratio}" + ) + + stablecoin = boa.load_partial("contracts/Stablecoin.vy").at( + "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + ) + assert stablecoin.balanceOf(controller_address) > user_state[2] + stablecoin.transfer(user, user_state[2], sender=controller_address) + stablecoin.approve(controller_address, 2**256 - 1, sender=user) + assert stablecoin.balanceOf(user) >= user_state[2] + + controller.liquidate_extended( + user, + 0, + int(frac * 10**18), + False, + "0x0000000000000000000000000000000000000000", + [], + sender=user, + ) + + user_state2 = controller.user_state(user) + debt_repayed = (user_state[2] - user_state2[2]) - (user_state[1] - user_state2[1]) + total_surplus = int(debt_repayed * (ratio - 1)) + collateral_used = user_state[0] - user_state2[0] + frac_price = (debt_repayed * ratio) / collateral_used + spot_surplus = collateral_used * spot_price - debt_repayed + health2 = controller.health(user) + t += "\n" + ( + f"total sulprus: {total_surplus / 1e18}, spot_surplus: {spot_surplus / 1e18}, spot_price: {spot_price}, frac_price: {frac_price}," + f" liquidator_profit:{(spot_surplus - total_surplus) / 1e18}, {(spot_surplus - total_surplus) * 100 / user_state[2]} % of position" + ) + t += "\n" + ( + f"user state after part liq: health: {health2 * 100 / 1e18}, collateral: {user_state2[0] / 1e18}, " + f"stablecoin: {user_state2[1] / 1e18}, debt: {user_state2[2] / 1e18}" + ) + + controller.repay(total_surplus, sender=user) + health3 = controller.health(user) + user_state3 = controller.user_state(user) + t += "\n" + ( + f"user state after repay: health: {health3 * 100 / 1e18}, collateral: {user_state3[0] / 1e18}, " + f"stablecoin: {user_state3[1] / 1e18}, debt: {user_state3[2] / 1e18}" + ) + + print(t) + + return t + + +res = "" + +for position in positions: + try: + res += ( + test_position(position, 0.05) + + "\n" + + "-----------------------------" + + "\n" + ) + except KeyboardInterrupt: + break + except: + continue + +with open("frac5_calc.txt", "w") as f: + f.write(res) diff --git a/tests/zaps/partial_liquidation/calculations/frac5_calc.txt b/tests/zaps/partial_liquidation/calculations/frac5_calc.txt new file mode 100644 index 00000000..fcddf90e --- /dev/null +++ b/tests/zaps/partial_liquidation/calculations/frac5_calc.txt @@ -0,0 +1,438 @@ +User: 0x81e3b8957e9Ab1b3ae1785Fd6ba7B1AcC2173490, block: 21758686, ratio: 1.0744636542044974 +user state: health: 0.9995834952227567, collateral: 0.027277767080616855, stablecoin: 0.0, debt: 58.075382270627095, ratio: 1.0744636542044974 +total sulprus: 0.21622525915969878, spot_surplus: 1.2742709980219535, spot_price: 3063.33, frac_price: 2287.5731459031826, liquidator_profit:1.0580457388622546, 1.821848944414757 % of position +user state after part liq: health: 0.9995834952227567, collateral: 0.025913878726586008, stablecoin: 0.0, debt: 55.17161315709574 +user state after repay: health: 3.546194394652552, collateral: 0.025913878726586, stablecoin: 0.0, debt: 54.95538789793604 +----------------------------- +User: 0x00Fb4599c681CA15FDF1AA537B066630876102aE, block: 20542247, ratio: 1.0746991403080577 +user state: health: 1.0217191889574198, collateral: 0.000122879325364173, stablecoin: 0.0, debt: 0.2564096505869242, ratio: 1.0746991403080577 +total sulprus: 0.000957679023276634, spot_surplus: 0.00297362723735062, spot_price: 2570.67, frac_price: 2242.551627263288, liquidator_profit:0.002015948214073986, 0.7862216611034181 % of position +user state after part liq: health: 1.0217191889574206, collateral: 0.000116735359095964, stablecoin: 0.0, debt: 0.24358916805757802 +user state after repay: health: 3.526291406877995, collateral: 0.00011673535909596, stablecoin: 0.0, debt: 0.24263148903430137 +----------------------------- +User: 0x0477011BBb0BD4a6A8a59182FF58dE328E3592a4, block: 20242554, ratio: 1.0742616450790734 +user state: health: 0.9805946374328974, collateral: 1.8069329585944525, stablecoin: 38294.63462018836, debt: 40555.91636479788, ratio: 1.0742616450790734 +total sulprus: 8.396325117099016, spot_surplus: 156.4137630159868, spot_price: 2982.71, frac_price: 1344.3820565656715, liquidator_profit:148.01743789888778, 0.3649712573807491 % of position +user state after part liq: health: 0.9805946374328974, collateral: 1.7165863106647299, stablecoin: 36379.90288917894, debt: 38528.120546557984 +user state after repay: health: 1.0026058516934069, collateral: 1.7165863106647299, stablecoin: 36379.90288917894, debt: 38519.72422144089 +----------------------------- +User: 0x10e50901A9F65629715db45B24e5b047a6F11240, block: 20267600, ratio: 1.0740127469416738 +user state: health: 0.957198212517341, collateral: 0.00691788594148839, stablecoin: 9.48138820634999, debt: 25.347700309019505, ratio: 1.0740127469416738 +total sulprus: 0.05871546712762478, spot_surplus: 0.26569858739033303, spot_price: 3061.67, frac_price: 2463.2700783667697, liquidator_profit:0.2069831202627083, 0.816575538369677 % of position +user state after part liq: health: 0.9571982125173413, collateral: 0.00657199164441398, stablecoin: 9.007318796032491, debt: 24.08031529356853 +user state after repay: health: 1.2039656674650923, collateral: 0.00657199164441398, stablecoin: 9.007318796032491, debt: 24.021599826440905 +----------------------------- +User: 0x1C79cb8Ce8C3695ed871E4D4e4519D937630832d, block: 20665126, ratio: 1.0748691059187516 +user state: health: 1.0376959563626464, collateral: 20.64709559300493, stablecoin: 4919.390870585533, debt: 50274.4075071779, ratio: 1.0748691059187516 +total sulprus: 169.78447722558883, spot_surplus: 337.9436026509937, spot_price: 2524.03, frac_price: 2361.141108758197, liquidator_profit:168.15912542540482, 0.3344825603392661 % of position +user state after part liq: health: 1.0376959563626464, collateral: 19.614740813354686, stablecoin: 4673.421327056257, debt: 47760.68713181901 +user state after repay: health: 1.39815628451684, collateral: 19.614740813354686, stablecoin: 4673.421327056257, debt: 47590.90265459342 +----------------------------- +User: 0x1CB82662c90260A6Cbe6Ab0B8298a3208B666b91, block: 18818673, ratio: 1.0742190868872157 +user state: health: 0.9765941673982794, collateral: 12.004984636781323, stablecoin: 6010.8037417598935, debt: 30105.49419778694, ratio: 1.0742190868872157 +total sulprus: 89.41429622382189, spot_surplus: 137.92096645322584, spot_price: 2236.83, frac_price: 2156.019117359155, liquidator_profit:48.50667022940396, 0.16112231844036526 % of position +user state after part liq: health: 0.9765941673982794, collateral: 11.404735404942256, stablecoin: 5710.263554671898, debt: 28600.219487897593 +user state after repay: health: 1.2932723896321827, collateral: 11.404735404942256, stablecoin: 5710.263554671898, debt: 28510.80519167377 +----------------------------- +User: 0x1d04Cd3BC5C9EDA103D7Feb2DB72350dB612E99a, block: 20563722, ratio: 1.0749038563371616 +user state: health: 1.0409624956931938, collateral: 1.4871433895724449, stablecoin: 2071.6165438732137, debt: 5470.13488271597, ratio: 1.0749038563371616 +total sulprus: 12.728106470594337, spot_surplus: 22.57364126259344, spot_price: 2588.85, frac_price: 2456.4413182140465, liquidator_profit:9.845534791999105, 0.17998705704877813 % of position +user state after part liq: health: 1.0409624956931938, collateral: 1.4127862200938228, stablecoin: 1968.035716679553, debt: 5196.628138580171 +user state after repay: health: 1.2890498663158005, collateral: 1.4127862200938228, stablecoin: 1968.035716679553, debt: 5183.900032109576 +----------------------------- +User: 0x1E268f0802321078A20153EcFA7Be65e4a078C59, block: 17939156, ratio: 1.0743805465791387 +user state: health: 0.9917713784390486, collateral: 1.9715043875618754, stablecoin: 70.27154625725963, debt: 3000.1153099830317, ratio: 1.0743805465791387 +total sulprus: 10.896169026870195, spot_surplus: 18.717907987008996, spot_price: 1675.98, frac_price: 1596.6320765615762, liquidator_profit:7.821738960138799, 0.2607146110055029 % of position +user state after part liq: health: 0.9917713784390486, collateral: 1.8729291681837816, stablecoin: 66.75796894439665, debt: 2850.10954448388 +user state after repay: health: 1.379351762771866, collateral: 1.8729291681837816, stablecoin: 66.75796894439665, debt: 2839.21337545701 +----------------------------- +User: 0x1e42E843e666Fc90844e609Bd66c32E2A2A0d5E8, block: 19570343, ratio: 1.0746777574549704 +user state: health: 1.0197092007672184, collateral: 55.897074726956255, stablecoin: 27843.39187795874, debt: 181473.22809228284, ratio: 1.0746777574549704 +total sulprus: 573.6365823330061, spot_surplus: 1477.774905505484, spot_price: 3277.19, frac_price: 2953.6888766982975, liquidator_profit:904.1383231724776, 0.4982213259096845 % of position +user state after part liq: health: 1.0197092007672184, collateral: 53.102220990608444, stablecoin: 26451.2222840608, debt: 172399.5666876687 +user state after repay: health: 1.3569609805112743, collateral: 53.102220990608444, stablecoin: 26451.2222840608, debt: 171825.9301053357 +----------------------------- +User: 0x201051Ae0FddaC0Ce47B299E4673cAA39f32A961, block: 21741988, ratio: 1.074335276639822 +user state: health: 0.9875160041432689, collateral: 1.2274323515080487, stablecoin: 297.3168466705183, debt: 3796.9415687032742, ratio: 1.074335276639822 +total sulprus: 13.00727859239325, spot_surplus: 23.501326015146983, spot_price: 3234.11, frac_price: 3063.1181337702965, liquidator_profit:10.494047422753733, 0.27638158851987926 % of position +user state after part liq: health: 0.9875160041432689, collateral: 1.1660607339326463, stablecoin: 282.4510043369924, debt: 3607.0944902681103 +user state after repay: health: 1.352997607024914, collateral: 1.1660607339326463, stablecoin: 282.4510043369924, debt: 3594.087211675717 +----------------------------- +User: 0x2098ed20eD0d7a78023977dDcd33DD8c596D1d03, block: 19956294, ratio: 1.0744324680357735 +user state: health: 0.9966519953627188, collateral: 4.6526066029028845, stablecoin: 49710.70933102092, debt: 62389.93287859189, ratio: 1.0744324680357735 +total sulprus: 47.18729507115018, spot_surplus: 264.61069365989397, spot_price: 3862.66, frac_price: 2928.029513712646, liquidator_profit:217.42339858874377, 0.3484911564367929 % of position +user state after part liq: health: 0.9966519953627188, collateral: 4.419976272757741, stablecoin: 47225.17386446987, debt: 59270.436234662295 +user state after repay: health: 1.0771230756265626, collateral: 4.419976272757741, stablecoin: 47225.17386446987, debt: 59223.24893959115 +----------------------------- +User: 0x20b9Efc68DB884Db2a6f804265f2F6ba611a0F41, block: 21725260, ratio: 1.0747633712462952 +user state: health: 1.0277568971517395, collateral: 23.930472241401983, stablecoin: 16041.678121502458, debt: 84037.10197603436, ratio: 1.0747633712462952 +total sulprus: 254.17835583427814, spot_surplus: 360.39998009265673, spot_price: 3142.58, frac_price: 3053.804798920094, liquidator_profit:106.22162425837857, 0.1263984856220658 % of position +user state after part liq: health: 1.0277568971517395, collateral: 22.73394862933188, stablecoin: 15239.594215427338, debt: 79835.24687723264 +user state after repay: health: 1.3504350116688673, collateral: 22.73394862933188, stablecoin: 15239.594215427338, debt: 79581.06852139835 +----------------------------- +User: 0x229A72082D79a6FC5EE65BCC9147a5B8501C9016, block: 20606655, ratio: 1.0742648415238878 +user state: health: 0.9808951032454435, collateral: 0.00014382241410903, stablecoin: 0.559895707737374, debt: 0.8883718891598416, ratio: 1.0742648415238878 +total sulprus: 0.001219711577885568, spot_surplus: 0.003451369890640128, spot_price: 2763.85, frac_price: 2453.5147401478584, liquidator_profit:0.00223165831275456, 0.251207668768662 % of position +user state after part liq: health: 0.9808951032454437, collateral: 0.000136631293403578, stablecoin: 0.5319009223505052, debt: 0.8439532947018494 +user state after repay: health: 1.1270475402304532, collateral: 0.000136631293403578, stablecoin: 0.5319009223505052, debt: 0.842733583123964 +----------------------------- +User: 0x26701C848abf9f2422A1FD5aA9B8b4544328A043, block: 19482899, ratio: 1.0747604916240299 +user state: health: 1.0274862126588016, collateral: 2.7445120106284104, stablecoin: 35371.016044360316, debt: 41824.67204055454, ratio: 1.0747604916240299 +total sulprus: 24.12392475239241, spot_surplus: 161.7029862261236, spot_price: 3529.85, frac_price: 2527.274234684039, liquidator_profit:137.5790614737312, 0.3289423556993588 % of position +user state after part liq: health: 1.0274862126588016, collateral: 2.60728641009699, stablecoin: 33602.4652421423, debt: 39733.438438526806 +user state after repay: health: 1.0888617237991947, collateral: 2.60728641009699, stablecoin: 33602.4652421423, debt: 39709.31451377442 +----------------------------- +User: 0x29D37036C9EE7e76413b93BEEe9e2c1b8E8Ad368, block: 20480142, ratio: 1.074842734979724 +user state: health: 1.0352170880940563, collateral: 0.000354717706346086, stablecoin: 2.5804417369735866, debt: 3.1717061930533013, ratio: 1.074842734979724 +total sulprus: 0.002212592449465239, spot_surplus: 0.012118590715625427, spot_price: 2350.14, frac_price: 1791.6114524289997, liquidator_profit:0.009905998266160189, 0.312323956356878 % of position +user state after part liq: health: 1.0352170880940563, collateral: 0.000336981821028781, stablecoin: 2.451419650124907, debt: 3.0131208834006364 +user state after repay: health: 1.1094637063424577, collateral: 0.000336981821028781, stablecoin: 2.451419650124907, debt: 3.010908290951171 +----------------------------- +User: 0x2F0126966657563C4a376cE43ba43891d7537A32, block: 19650067, ratio: 1.0748081167509347 +user state: health: 1.0319629745878525, collateral: 12.141198513270195, stablecoin: 30197.54749493003, debt: 58617.0783310624, ratio: 1.0748081167509347 +total sulprus: 106.30057903980892, spot_surplus: 404.14912470072375, spot_price: 3006.5, frac_price: 2515.8589066427508, liquidator_profit:297.84854566091485, 0.5081258809569135 % of position +user state after part liq: health: 1.0319629745878525, collateral: 11.534138587606686, stablecoin: 28687.67012018353, debt: 55686.224414509285 +user state after repay: health: 1.2251938289071138, collateral: 11.534138587606686, stablecoin: 28687.67012018353, debt: 55579.923835469475 +----------------------------- +User: 0x314e5699db4756138107AE7d7EeDDf5708583ff5, block: 21768935, ratio: 1.0742488618505643 +user state: health: 0.9793930139530443, collateral: 0.4422578126799306, stablecoin: 0.0, debt: 1000.0344720446249, ratio: 1.0742488618505643 +total sulprus: 3.71257106803217, spot_surplus: 13.123503032825955, spot_price: 2854.68, frac_price: 2429.0942129330956, liquidator_profit:9.410931964793784, 0.9410607561909964 % of position +user state after part liq: health: 0.9793930139530443, collateral: 0.420144922045934, stablecoin: 0.0, debt: 950.0327484423937 +user state after repay: health: 3.5613785157720725, collateral: 0.420144922045934, stablecoin: 0.0, debt: 946.3201773743614 +----------------------------- +User: 0x3182696B6B106a68a3062a8798183D87f20Ed598, block: 18871481, ratio: 1.0740690339171493 +user state: health: 0.9624891882120221, collateral: 17.22654772477257, stablecoin: 5458.141493876267, debt: 39990.49317869601, ratio: 1.0740690339171493 +total sulprus: 127.88889640909198, spot_surplus: 171.5498697301151, spot_price: 2203.77, frac_price: 2153.079665501653, liquidator_profit:43.660973321023114, 0.10917838178670541 % of position +user state after part liq: health: 0.9624891882120221, collateral: 16.36522033853394, stablecoin: 5185.234419182453, debt: 37990.968519761205 +user state after repay: health: 1.3035069144360623, collateral: 16.36522033853394, stablecoin: 5185.234419182453, debt: 37863.07962335212 +----------------------------- +User: 0x369F54c03447B0f3af7760CE730a1364D1c23e1E, block: 19098969, ratio: 1.074521666428889 +user state: health: 1.0050366443155618, collateral: 0.00064723797715504, stablecoin: 0.7498234463711074, debt: 2.016512862192869, ratio: 1.074521666428889 +total sulprus: 0.004719790305743682, spot_surplus: 0.010107622476692031, spot_price: 2269.4, frac_price: 2102.9131014829836, liquidator_profit:0.00538783217094835, 0.26718560897694044 % of position +user state after part liq: health: 1.0050366443155623, collateral: 0.000614876078297289, stablecoin: 0.7123322740525521, debt: 1.915687219083226 +user state after repay: health: 1.2545032681868562, collateral: 0.000614876078297289, stablecoin: 0.7123322740525521, debt: 1.9109674287774823 +----------------------------- +User: 0x442ABE3cef7665cEC0E57715EE8BA686762865ae, block: 20223450, ratio: 1.0743752459944373 +user state: health: 0.9912731234771034, collateral: 11.86891288872027, stablecoin: 0.0, debt: 35297.35618710719, ratio: 1.0743752459944373 +total sulprus: 131.26247746846857, spot_surplus: 226.3975093545738, spot_price: 3355.43, frac_price: 3195.1204033620165, liquidator_profit:95.13503188610521, 0.26952452580812414 % of position +user state after part liq: health: 0.9912731234771034, collateral: 11.275467244284258, stablecoin: 0.0, debt: 33532.488377751826 +user state after repay: health: 3.4826946150466735, collateral: 11.275467244284258, stablecoin: 0.0, debt: 33401.22590028335 +----------------------------- +User: 0x4Cb43773C56a85991f3201A316C650cA88acC873, block: 20159005, ratio: 1.0747774536785 +user state: health: 1.0290806457789896, collateral: 0.000767769480354029, stablecoin: 0.7289695869828998, debt: 3.038069664116084, ratio: 1.0747774536785 +total sulprus: 0.008633431202842368, spot_surplus: 0.015200551347316129, spot_price: 3403.51, frac_price: 3232.43989855637, liquidator_profit:0.006567120144473759, 0.2161609466050358 % of position +user state after part liq: health: 1.02908064577899, collateral: 0.000729381006336331, stablecoin: 0.6925211076337549, debt: 2.88616618091028 +user state after repay: health: 1.3321971324047033, collateral: 0.000729381006336331, stablecoin: 0.6925211076337549, debt: 2.8775327497074374 +----------------------------- +User: 0x4cC7303602376679a0E69434f3B25703e465F535, block: 20480142, ratio: 1.0748573398590504 +user state: health: 1.03658994675073, collateral: 6.022898654221e-05, stablecoin: 0.4381438757317187, debt: 0.5385297958138544, ratio: 1.0748573398590504 +total sulprus: 0.000375731146832595, spot_surplus: 0.002058031517509858, spot_price: 2350.14, frac_price: 1791.505207266775, liquidator_profit:0.001682300370677263, 0.31238761230191225 % of position +user state after part liq: health: 1.0365899467507307, collateral: 5.7217537215099e-05, stablecoin: 0.4162366819451328, debt: 0.5116033060231616 +user state after repay: health: 1.1108476661664544, collateral: 5.7217537215099e-05, stablecoin: 0.4162366819451328, debt: 0.5112275748763291 +----------------------------- +User: 0x4da79bBf1f9cB0086684E8Eb0fb5e559952Bd0BC, block: 18355636, ratio: 1.074158613581051 +user state: health: 0.9709096766188035, collateral: 1.1908867432588714, stablecoin: 906.1159060585265, debt: 2512.291956720936, ratio: 1.074158613581051 +total sulprus: 5.955589454210615, spot_surplus: 12.270137444452548, spot_price: 1554.79, frac_price: 1448.7421658799874, liquidator_profit:6.314547990241934, 0.2513461054297102 % of position +user state after part liq: health: 0.9709096766188035, collateral: 1.1313424060959278, stablecoin: 860.8101107556002, debt: 2386.677358884889 +user state after repay: health: 1.2234974811130375, collateral: 1.1313424060959278, stablecoin: 860.8101107556002, debt: 2380.7217694306787 +----------------------------- +User: 0x547E826F342f0355055E9424D5797D5Bc5F73221, block: 20694185, ratio: 1.0747212863769346 +user state: health: 1.023800919431867, collateral: 185.7420514546439, stablecoin: 322872.09696378745, debt: 669789.34872943, ratio: 1.0747212863769346 +total sulprus: 1296.1051659139862, spot_surplus: 3019.8246434622974, spot_price: 2192.9, frac_price: 2007.2964208375051, liquidator_profit:1723.719477548311, 0.2573524766881042 % of position +user state after part liq: health: 1.023800919431867, collateral: 176.45494888191172, stablecoin: 306728.4921155981, debt: 636299.8812929585 +user state after repay: health: 1.2300004337883694, collateral: 176.45494888191172, stablecoin: 306728.4921155981, debt: 635003.7761270446 +----------------------------- +User: 0x5e52b108018Dd567A34b0B403b95cBA848ab974C, block: 19113233, ratio: 1.0747601565976046 +user state: health: 1.0274547201748412, collateral: 0.001732638950048313, stablecoin: 2.6945201333634663, debt: 6.023217627190179, ratio: 1.0747601565976046 +total sulprus: 0.012442697295226955, spot_surplus: 0.029773491609506656, spot_price: 2264.85, frac_price: 2064.7991548565637, liquidator_profit:0.017330794314279703, 0.2877331583711095 % of position +user state after part liq: health: 1.0274547201748414, collateral: 0.001646007002545899, stablecoin: 2.559794126695293, debt: 5.72205674583067 +user state after repay: health: 1.2476191703293709, collateral: 0.001646007002545899, stablecoin: 2.559794126695293, debt: 5.709614048535443 +----------------------------- +User: 0x5Eae7CDc3A9F357B1Ca1f4918dB664A9E7CD5FF6, block: 19500711, ratio: 1.0746022021901134 +user state: health: 1.0126070058706607, collateral: 0.009558287574235496, stablecoin: 158.70610155091273, debt: 177.38014051878872, ratio: 1.0746022021901134 +total sulprus: 0.06965622153937703, spot_surplus: 0.6633301935159778, spot_price: 3341.67, frac_price: 2099.4517315794988, liquidator_profit:0.5936739719766007, 0.33469021404553273 % of position +user state after part liq: health: 1.0126070058706607, collateral: 0.009080373195523721, stablecoin: 150.7707964733671, debt: 168.51113349284927 +user state after repay: health: 1.054379119520185, collateral: 0.009080373195523721, stablecoin: 150.7707964733671, debt: 168.4414772713099 +----------------------------- +User: 0x5fF910c34Db2Ae9Eeee29cf6902197CBC7B6812D, block: 17940349, ratio: 1.0745473807876664 +user state: health: 1.0074537940406416, collateral: 50.488713604325866, stablecoin: 49765.087141821714, debt: 120085.14483115019, ratio: 1.0745473807876664 +total sulprus: 262.108805878852, spot_surplus: 753.7771363378109, spot_price: 1691.38, frac_price: 1496.6163408138675, liquidator_profit:491.66833045895885, 0.40943309944813394 % of position +user state after part liq: health: 1.0074537940406416, collateral: 47.964277924109574, stablecoin: 47276.832784730635, debt: 114080.88758959269 +user state after repay: health: 1.240059901589281, collateral: 47.964277924109574, stablecoin: 47276.832784730635, debt: 113818.77878371385 +----------------------------- +User: 0x60Fa6eEf5415aD3a7FbEC63F3419eb5F590b88cb, block: 20111365, ratio: 1.0745143533638326 +user state: health: 1.0043492162002716, collateral: 1.10924137414e-06, stablecoin: 0.007245503218695266, debt: 0.010320905064853001, ratio: 1.0745143533638326 +total sulprus: 1.1458078995019e-05, spot_surplus: 4.146415423036097e-05, spot_price: 3520.14, frac_price: 2979.120237577147, liquidator_profit:3.0006075235341967e-05, 0.2907310458413691 % of position +user state after part liq: health: 1.0043492162004186, collateral: 1.053779305433e-06, stablecoin: 0.006883228057760514, debt: 0.009804859811610353 +user state after repay: health: 1.1225222314363257, collateral: 1.053779305433e-06, stablecoin: 0.006883228057760514, debt: 0.009793401732615331 +----------------------------- +User: 0x654adAa768FD13c3904fD64B56E1d2A530447D47, block: 18962158, ratio: 1.0747248252128776 +user state: health: 1.0241335700104846, collateral: 3.41677867887849, stablecoin: 6296.524181315191, debt: 12827.329895672603, ratio: 1.0747248252128776 +total sulprus: 24.400665775230998, spot_surplus: 53.89410272449711, spot_price: 2226.86, frac_price: 2054.221150831421, liquidator_profit:29.493436949266115, 0.2299265489321823 % of position +user state after part liq: health: 1.0241335700104846, collateral: 3.2459397449345655, stablecoin: 5981.69797224943, debt: 12185.963400888973 +user state after repay: health: 1.226825951916046, collateral: 3.2459397449345655, stablecoin: 5981.69797224943, debt: 12161.562735113743 +----------------------------- +User: 0x6764E71d06f5947784B81718A834afFaf548b5cB, block: 18381864, ratio: 1.0748864659418098 +user state: health: 1.0393277985301195, collateral: 0.000641798740525388, stablecoin: 0.5329601142499027, debt: 1.3939480493909493, ratio: 1.0748864659418098 +total sulprus: 0.003223817184062457, spot_surplus: 0.00663513184135124, spot_price: 1548.29, frac_price: 1441.9851900374201, liquidator_profit:0.003411314657288783, 0.24472322758220946 % of position +user state after part liq: health: 1.0393277985301195, collateral: 0.000609708803499118, stablecoin: 0.5063121085374076, debt: 1.3242506469214022 +user state after repay: health: 1.2859028974535731, collateral: 0.000609708803499118, stablecoin: 0.5063121085374076, debt: 1.3210268297373398 +----------------------------- +User: 0x6943A928EE855BbE7A7F96Dab4178Dfda3fB91e0, block: 20285517, ratio: 1.0748709951709863 +user state: health: 1.0378735460727058, collateral: 0.28556093738531824, stablecoin: 266.4248249299873, debt: 1010.3421091594265, ratio: 1.0748709951709863 +total sulprus: 2.7848913697577786, spot_surplus: 7.240416035526107, spot_price: 3112.21, frac_price: 2800.1557879243255, liquidator_profit:4.455524665768329, 0.44099168245844844 % of position +user state after part liq: health: 1.037873546072706, collateral: 0.27128289051605237, stablecoin: 253.10358368348795, debt: 959.8250037014551 +user state after repay: health: 1.331883690926084, collateral: 0.27128289051605237, stablecoin: 253.10358368348795, debt: 957.0401123316974 +----------------------------- +User: 0x7021528C73E008E06E7D83a1e0697D0b072F0D0B, block: 20472964, ratio: 1.0740374570020175 +user state: health: 0.959520958189646, collateral: 3.4279440775624e-05, stablecoin: 0.08114011488838721, debt: 0.14668765304637382, ratio: 1.0740374570020175 +total sulprus: 0.000242648651898003, spot_surplus: 0.0009423365529849195, spot_price: 2461.95, frac_price: 2053.724028248839, liquidator_profit:0.0006996879010869165, 0.47699168031934985 % of position +user state after part liq: health: 0.9595209581896745, collateral: 3.256546873684e-05, stablecoin: 0.07708310914396788, debt: 0.13935327039405512 +user state after repay: health: 1.1356231950280635, collateral: 3.256546873684e-05, stablecoin: 0.07708310914396788, debt: 0.13911062174215713 +----------------------------- +User: 0x794F2331F69f9D276a3A006953669CD2FC23Ab92, block: 20462218, ratio: 1.0749024039204325 +user state: health: 1.0408259685206434, collateral: 0.6960930617210745, stablecoin: 628.6431522510542, debt: 2000.2511449497026, ratio: 1.0749024039204325 +total sulprus: 5.136836794480387, spot_surplus: 11.096500488846951, spot_price: 2289.26, frac_price: 2118.0281914360294, liquidator_profit:5.959663694366563, 0.29794577093050095 % of position +user state after part liq: health: 1.0408259685206434, collateral: 0.6612884086350208, stablecoin: 597.2109946385016, debt: 1900.2385877022175 +user state after repay: health: 1.3147058445384747, collateral: 0.6612884086350208, stablecoin: 597.2109946385016, debt: 1895.101750907737 +----------------------------- +User: 0x79A47EA2bF6C0f036E8EB1022a7693e2cffD5C50, block: 19754870, ratio: 1.0749756337969776 +user state: health: 1.0477095769158973, collateral: 23.809723198535284, stablecoin: 5.1978009016997735, debt: 60978.17311261469, ratio: 1.0749756337969776 +total sulprus: 228.57437342415741, spot_surplus: 902.9438499208582, spot_price: 3319.31, frac_price: 2752.8443835175826, liquidator_profit:674.3694764967007, 1.1059194496549987 % of position +user state after part liq: health: 1.0477095769158973, collateral: 22.61923703860852, stablecoin: 4.937910856614785, debt: 57929.26445698396 +user state after repay: health: 1.4479979767433824, collateral: 22.61923703860852, stablecoin: 4.937910856614785, debt: 57700.690083559806 +----------------------------- +User: 0x7BaE493fb2f56F43cdc535d6Ad6C845f8C2B35e2, block: 19703638, ratio: 1.0739972606152957 +user state: health: 0.9557424978377862, collateral: 1.61577367326e-06, stablecoin: 0.010368488063944232, debt: 0.014337191837129266, ratio: 1.0739972606152957 +total sulprus: 1.4683660370464e-05, spot_surplus: 5.769886980276488e-05, spot_price: 3170.42, frac_price: 2637.9789763140484, liquidator_profit:4.3015209432300875e-05, 0.30002534611348136 % of position +user state after part liq: health: 0.9557424978378078, collateral: 1.534984989596e-06, stablecoin: 0.009850063660747024, debt: 0.013620332245272804 +user state after repay: health: 1.0646972327801194, collateral: 1.534984989596e-06, stablecoin: 0.009850063660747024, debt: 0.01360564858490234 +----------------------------- +User: 0x81e3b8957e9Ab1b3ae1785Fd6ba7B1AcC2173490, block: 21031734, ratio: 1.0744636542044974 +user state: health: 0.9995834952227566, collateral: 0.027277767080616855, stablecoin: 0.0, debt: 55.84522179473992, ratio: 1.0744636542044974 +total sulprus: 0.20792196423484877, spot_surplus: 0.6483109499082942, spot_price: 2522.62, frac_price: 2199.727745386991, liquidator_profit:0.44038898567344537, 0.7885884799457019 % of position +user state after part liq: health: 0.9995834952227566, collateral: 0.025913878726586008, stablecoin: 0.0, debt: 53.052960705002924 +user state after repay: health: 3.5461943946525456, collateral: 0.025913878726586, stablecoin: 0.0, debt: 52.84503874076808 +----------------------------- +User: 0x83A6851f146A272Ff257afd7509f9747A87FB689, block: 17736621, ratio: 1.0745404498987816 +user state: health: 1.0068022904854725, collateral: 0.6273971192148342, stablecoin: 697.0907577500843, debt: 1726.3342596189484, ratio: 1.0745404498987816 +total sulprus: 3.8360136842351302, spot_surplus: 7.74089926702619, spot_price: 1887.26, frac_price: 1762.781086622843, liquidator_profit:3.90488558279106, 0.22619522036554962 % of position +user state after part liq: health: 1.0068022904854725, collateral: 0.5960272632540925, stablecoin: 662.23621986258, debt: 1640.017546638001 +user state after repay: health: 1.2436118791425788, collateral: 0.5960272632540925, stablecoin: 662.23621986258, debt: 1636.181532953766 +----------------------------- +User: 0x84834D3B6844E25CE6911a50897EC073Fb489568, block: 18136740, ratio: 1.074285514455537 +user state: health: 0.9828383588204781, collateral: 0.4061625734401971, stablecoin: 14.148576644947365, debt: 600.9968461735674, ratio: 1.074285514455537 +total sulprus: 2.179716280463759, spot_surplus: 3.850410512822227, spot_price: 1634.46, frac_price: 1552.192733559979, liquidator_profit:1.6706942323584681, 0.27798718795206 % of position +user state after part liq: health: 0.9828383588204781, collateral: 0.38585444476818725, stablecoin: 13.441147812699999, debt: 570.9470038648891 +user state after repay: health: 1.369840110895554, collateral: 0.38585444476818725, stablecoin: 13.441147812699999, debt: 568.7672875844253 +----------------------------- +User: 0x8a625BF21c01A83d93D1175556Ef3aF76b862c83, block: 18087987, ratio: 1.0740952501442722 +user state: health: 0.9649535135615939, collateral: 4.508917896868007, stablecoin: 423.15942970989636, debt: 6969.204745679922, ratio: 1.0740952501442722 +total sulprus: 24.25154325712702, spot_surplus: 43.79745832637471, spot_price: 1646.07, frac_price: 1559.37108236025, liquidator_profit:19.545915069247688, 0.28046119726018715 % of position +user state after part liq: health: 0.9649535135615939, collateral: 4.283472002024606, stablecoin: 402.00145822440146, debt: 6620.744508395926 +user state after repay: health: 1.336144076566528, collateral: 4.283472002024606, stablecoin: 402.00145822440146, debt: 6596.492965138798 +----------------------------- +User: 0x8d46f6E3B726D17139238d8d1d372838Be422dBE, block: 20403708, ratio: 1.0744824199737675 +user state: health: 1.001347477534148, collateral: 6.243244527564e-06, stablecoin: 0.015012093719643108, debt: 0.031488118418568245, ratio: 1.0744824199737675 +total sulprus: 6.1358709556176e-05, spot_surplus: 0.00018442344544135163, spot_price: 3229.81, frac_price: 2835.5767280506493, liquidator_profit:0.00012306473588517562, 0.3908291192547266 % of position +user state after part liq: health: 1.0013474775342242, collateral: 5.931082301183e-06, stablecoin: 0.014261489033660962, debt: 0.029913712497639838 +user state after repay: health: 1.2089462614910942, collateral: 5.931082301183e-06, stablecoin: 0.014261489033660962, debt: 0.02985235378808366 +----------------------------- +User: 0x8E53B23D255208CBF2Ff86Eb282f30CEB61539ED, block: 20692603, ratio: 1.0747540502405557 +user state: health: 1.0268807226122427, collateral: 0.000528226963896854, stablecoin: 0.11966248340086189, debt: 1.2005478637137623, ratio: 1.0747540502405557 +total sulprus: 0.004040028001209635, spot_surplus: 0.00638700856201364, spot_price: 2288.08, frac_price: 2199.2174192831935, liquidator_profit:0.002346980560804005, 0.19549246071238507 % of position +user state after part liq: health: 1.0268807226122434, collateral: 0.00050181561570201, stablecoin: 0.11367935923081879, debt: 1.140520470528074 +user state after repay: health: 1.3860170629497246, collateral: 0.00050181561570201, stablecoin: 0.11367935923081879, debt: 1.1364804425268644 +----------------------------- +User: 0x8Fee6B44B975D9BF99728Ae22E1FEDEc38De2Bcf, block: 20128026, ratio: 1.0748680045592143 +user state: health: 1.037592428566151, collateral: 0.000731846151267141, stablecoin: 3.4697728277703965, debt: 5.558842854688784, ratio: 1.0748680045592143 +total sulprus: 0.007820225214992189, spot_surplus: 0.025295136889152657, spot_price: 3545.79, frac_price: 3068.2330259309456, liquidator_profit:0.01747491167416047, 0.31436239755222967 % of position +user state after part liq: health: 1.0375924285661513, collateral: 0.000695253843703785, stablecoin: 3.2962841863818766, debt: 5.280900711954346 +user state after repay: health: 1.1874359080956214, collateral: 0.000695253843703785, stablecoin: 3.2962841863818766, debt: 5.273080486739354 +----------------------------- +User: 0x93285233955DdD615f1bAEaa7825D662152c6F24, block: 19476963, ratio: 1.0746745131826312 +user state: health: 1.0194042391673288, collateral: 6.382259151074121, stablecoin: 1805.8539962001992, debt: 20204.140182939373, ratio: 1.0746745131826312 +total sulprus: 68.69415321947378, spot_surplus: 137.66432555092962, spot_price: 3314.12, frac_price: 3097.9890949431338, liquidator_profit:68.97017233145583, 0.34136653035943154 % of position +user state after part liq: health: 1.0194042391673288, collateral: 6.0631461935204145, stablecoin: 1715.5612963901892, debt: 19193.9331737924 +user state after repay: health: 1.3822463675967092, collateral: 6.0631461935204145, stablecoin: 1715.5612963901892, debt: 19125.239020572928 +----------------------------- +User: 0x944cF2acB0DB10d0863fE45C4916B2fd7005C6a4, block: 18399734, ratio: 1.0740999920747505 +user state: health: 0.9653992550265501, collateral: 0.00384354842915413, stablecoin: 1.6259482501489753, debt: 7.042466011094611, ratio: 1.0740999920747505 +total sulprus: 0.02006819615794084, spot_surplus: 0.041481639563636256, spot_price: 1625.1, frac_price: 1513.6746137955752, liquidator_profit:0.021413443405695416, 0.3040617217315774 % of position +user state after part liq: health: 0.9653992550265502, collateral: 0.003651371007696424, stablecoin: 1.5446508376415264, debt: 6.6903427105398805 +user state after repay: health: 1.2691638801624365, collateral: 0.003651371007696424, stablecoin: 1.5446508376415264, debt: 6.67027451438194 +----------------------------- +User: 0x9C398b892B492787E79FB998078b56Ba0F6A250B, block: 20699769, ratio: 1.0747902553113304 +user state: health: 1.0302839992650603, collateral: 0.000121370186183082, stablecoin: 0.1943458611549893, debt: 0.4298769702982935, ratio: 1.0747902553113304 +total sulprus: 0.000880771589329427, spot_surplus: 0.002204379714726926, spot_price: 2303.85, frac_price: 2085.739083798367, liquidator_profit:0.001323608125397499, 0.30790393923150655 % of position +user state after part liq: health: 1.030283999265062, collateral: 0.000115301676873929, stablecoin: 0.18462856809723985, debt: 0.4083831217833788 +user state after repay: health: 1.2486498657835308, collateral: 0.000115301676873929, stablecoin: 0.18462856809723985, debt: 0.4075023501940494 +----------------------------- +User: 0xA4B738b8E5dbb479FdB7489958F9dD5569FbbCEa, block: 20287898, ratio: 1.0748534704082542 +user state: health: 1.0362262183758937, collateral: 0.000311415356551817, stablecoin: 0.845763169064243, debt: 1.6602720268824778, ratio: 1.0748534704082542 +total sulprus: 0.003048440734297908, spot_surplus: 0.007359423775863408, spot_price: 3088.15, frac_price: 2811.286129874948, liquidator_profit:0.0043109830415655, 0.2596552234672236 % of position +user state after part liq: health: 1.0362262183758941, collateral: 0.000295844588724226, stablecoin: 0.8034750106110308, debt: 1.577258425538354 +user state after repay: health: 1.231881785687121, collateral: 0.000295844588724226, stablecoin: 0.8034750106110308, debt: 1.574209984804056 +----------------------------- +User: 0xa8c76bE1297B81b0CE3874D7E8B4a44F7d1f7E0b, block: 20456791, ratio: 1.0749506698530167 +user state: health: 1.0453629661835586, collateral: 54.01177271509101, stablecoin: 33983.241271712875, debt: 166477.03396152626, ratio: 1.0749506698530167 +total sulprus: 496.524925673412, spot_surplus: 637.2202129396733, spot_price: 2689.01, frac_price: 2636.9119924013894, liquidator_profit:140.69528726626135, 0.08451333130957678 % of position +user state after part liq: health: 1.0453629661835586, collateral: 51.311184079336456, stablecoin: 32284.07920812723, debt: 158153.18226344997 +user state after repay: health: 1.3635959046692172, collateral: 51.311184079336456, stablecoin: 32284.07920812723, debt: 157656.65733777653 +----------------------------- +User: 0xAb40e16d0156D14169D0feF043B3f2FcC6A43fD0, block: 19439013, ratio: 1.0742150689270917 +user state: health: 0.9762164791466249, collateral: 0.1322778619123595, stablecoin: 1336.3966605833918, debt: 1700.8124415132622, ratio: 1.0742150689270917 +total sulprus: 1.3522571149915155, spot_surplus: 6.608956135213553, spot_price: 3754.18, frac_price: 2959.3835096084545, liquidator_profit:5.256699020222038, 0.3090698828346411 % of position +user state after part liq: health: 0.9762164791466249, collateral: 0.12566396881674152, stablecoin: 1269.576827554222, debt: 1615.7718194375993 +user state after repay: health: 1.060795364564989, collateral: 0.12566396881674152, stablecoin: 1269.576827554222, debt: 1614.4195623226078 +----------------------------- +User: 0xB221963CAD5856c657647D7126A6Fe6A47CaC773, block: 19641764, ratio: 1.0745962533399378 +user state: health: 1.0120478139541509, collateral: 7.337699760531661, stablecoin: 198131.6474486807, debt: 206324.27992283413, ratio: 1.0745962533399378 +total sulprus: 30.55698437814751, spot_surplus: 770.4686156793543, spot_price: 3216.54, frac_price: 1199.7999985050474, liquidator_profit:739.9116313012069, 0.358615879613362 % of position +user state after part liq: health: 1.0120478139541509, collateral: 6.970814772505078, stablecoin: 188225.06507624668, debt: 196008.06592669245 +user state after repay: health: 1.0277977006820418, collateral: 6.970814772505078, stablecoin: 188225.06507624668, debt: 195977.50894231428 +----------------------------- +User: 0xb29E391A1124Ab6c6e68A210cdFC5824c8E2A4B5, block: 21379122, ratio: 1.0740779745781694 +user state: health: 0.963329610347918, collateral: 1.979639945912637, stablecoin: 19451.208521013825, debt: 24688.765280161402, ratio: 1.0740779745781694 +total sulprus: 19.39937982279269, spot_surplus: 105.65518566095064, spot_price: 3713.13, frac_price: 2841.7007684748396, liquidator_profit:86.25580583815795, 0.3493726999278842 % of position +user state after part liq: health: 0.963329610347918, collateral: 1.880657948617005, stablecoin: 18478.648094963133, debt: 23454.32701615333 +user state after repay: health: 1.046906825079682, collateral: 1.880657948617005, stablecoin: 18478.648094963133, debt: 23434.927636330536 +----------------------------- +User: 0xb820Fd41dbFEd079Ea2F612399361E5033Dd7af7, block: 20458453, ratio: 1.0748986281084605 +user state: health: 1.0404710421952867, collateral: 7.042380472212421, stablecoin: 104915.61413785926, debt: 115408.51113213705, ratio: 1.0748986281084605 +total sulprus: 39.295179487739716, spot_surplus: 426.977938735231, spot_price: 2702.56, frac_price: 1601.56081150913, liquidator_profit:387.6827592474913, 0.33592215638551454 % of position +user state after part liq: health: 1.0404710421952867, collateral: 6.6902614486018, stablecoin: 99669.83343096629, debt: 109638.08557553019 +user state after repay: health: 1.0766977508184523, collateral: 6.6902614486018, stablecoin: 99669.83343096629, debt: 109598.79039604246 +----------------------------- +User: 0xC3b876976D58dD9c2e8ab8ce0446C1D5eD8bF55c, block: 18796120, ratio: 1.0748587638474338 +user state: health: 1.0367238016587808, collateral: 3.0706198460364673, stablecoin: 56579.211341703776, debt: 58930.64310435155, ratio: 1.0748587638474338 +total sulprus: 8.801263751170227, spot_surplus: 226.9596351425182, spot_price: 2244.05, frac_price: 823.1097186887517, liquidator_profit:218.15837139134797, 0.3701951309186354 % of position +user state after part liq: health: 1.0367238016587808, collateral: 2.9170888537346444, stablecoin: 53750.250774618595, debt: 55984.110949133974 +user state after repay: health: 1.0526102855340946, collateral: 2.9170888537346444, stablecoin: 53750.250774618595, debt: 55975.309685382796 +----------------------------- +User: 0xc3C6B0C4F9C3871b072DC087336A5391f9BF3c07, block: 18844809, ratio: 1.0743805055829825 +user state: health: 0.9917675248003488, collateral: 0.0584419253156908, stablecoin: 54.51209361050825, debt: 174.89575799884034, ratio: 1.0743805055829825 +total sulprus: 0.44770989105681114, spot_surplus: 0.7342195652513361, spot_price: 2311.15, frac_price: 2213.1006381260163, liquidator_profit:0.28650967419452505, 0.16381739470000453 % of position +user state after part liq: health: 0.9917675248003488, collateral: 0.05551982904990626, stablecoin: 51.78648892998284, debt: 166.1509700988983 +user state after repay: health: 1.2646348973520414, collateral: 0.05551982904990626, stablecoin: 51.78648892998284, debt: 165.7032602078415 +----------------------------- +User: 0xC4aFfC415D30b7518c724114F6374172F97F4C0f, block: 20112551, ratio: 1.0742222106571648 +user state: health: 0.9768878017734881, collateral: 9.989781800675512, stablecoin: 50808.419572498715, debt: 79746.47591564579, ratio: 1.0742222106571648 +total sulprus: 107.39232569549836, spot_surplus: 302.258027231925, spot_price: 3501.9, frac_price: 3111.7699542701725, liquidator_profit:194.86570153642663, 0.2443565051608696 % of position +user state after part liq: health: 0.9768878017734881, collateral: 9.490292710641736, stablecoin: 48267.99859387378, debt: 75759.1521198635 +user state after repay: health: 1.120230704199425, collateral: 9.490292710641736, stablecoin: 48267.99859387378, debt: 75651.759794168 +----------------------------- +User: 0xC773B66E3766FF7515bbA8906f99E4BfBA958D65, block: 18621189, ratio: 1.0742420952142182 +user state: health: 0.9787569501365108, collateral: 0.41945935574099685, stablecoin: 7543.184883989256, debt: 7769.605619159732, ratio: 1.0742420952142182 +total sulprus: 0.8404974889499904, spot_surplus: 30.413071840926655, spot_price: 1989.9, frac_price: 579.8671113671949, liquidator_profit:29.572574351976666, 0.3806187315228863 % of position +user state after part liq: health: 0.9787569501365108, collateral: 0.39848638795394703, stablecoin: 7166.025639789793, debt: 7381.125338201746 +user state after repay: health: 0.9902568303559962, collateral: 0.39848638795394703, stablecoin: 7166.025639789793, debt: 7380.284840712796 +----------------------------- +User: 0xc99989c2913BF8Cf1978E6fd7fcDb587a4D3fd3b, block: 21472162, ratio: 1.0749773851178939 +user state: health: 1.0478742010820257, collateral: 102.80373162929733, stablecoin: 66691.53541235015, debt: 381825.6327193455, ratio: 1.0749773851178939 +total sulprus: 1181.3965288783218, spot_surplus: 1767.9388543016616, spot_price: 3409.34, frac_price: 3295.2308492663733, liquidator_profit:586.54232542334, 0.1536152303987585 % of position +user state after part liq: health: 1.0478742010820257, collateral: 97.66354504783246, stablecoin: 63356.958641732635, debt: 362734.3510833782 +user state after repay: health: 1.3780543484930372, collateral: 97.66354504783246, stablecoin: 63356.958641732635, debt: 361552.9545544999 +----------------------------- +User: 0xCD67353EcC3755C1f4f3976a1e1929fB5e61aa33, block: 18392585, ratio: 1.0740866140515062 +user state: health: 0.9641417208415879, collateral: 2.185269186e-09, stablecoin: 3.38566027256e-07, debt: 3.447994039725e-06, ratio: 1.0740866140515062 +total sulprus: 1.1518349654e-08, spot_surplus: 1.9739114898160004e-08, spot_price: 1603.56, frac_price: 1528.321991164121, liquidator_profit:8.220765244160004e-09, 0.2384216779219161 % of position +user state after part liq: health: 0.9641417208597094, collateral: 2.076005725e-09, stablecoin: 3.21637725893e-07, debt: 3.275594337739e-06 +user state after repay: health: 1.3204264063578421, collateral: 2.076005725e-09, stablecoin: 3.21637725893e-07, debt: 3.264075988085e-06 +----------------------------- +User: 0xD3Dd68F794174cbadf0dA25fb15cdf9D4D673D45, block: 19104897, ratio: 1.074059602438184 +user state: health: 0.9616026291893154, collateral: 0.0005072613149743, stablecoin: 0.942978445587019, debt: 1.9278418282117997, ratio: 1.074059602438184 +total sulprus: 0.003646929528655823, spot_surplus: 0.00858309899188624, spot_price: 2279.94, frac_price: 2085.3196212123726, liquidator_profit:0.004936169463230417, 0.2560463929662238 % of position +user state after part liq: health: 0.9616026291893157, collateral: 0.000481898249225585, stablecoin: 0.895829523307668, debt: 1.83144973680121 +user state after repay: health: 1.163046597008926, collateral: 0.000481898249225585, stablecoin: 0.895829523307668, debt: 1.827802807272554 +----------------------------- +User: 0xd416B5510645b532E1414fa71F4aD895abDc4D44, block: 18809168, ratio: 1.0741311701837128 +user state: health: 0.9683299972690153, collateral: 0.01546723952628986, stablecoin: 17.403502252221084, debt: 47.05190864466438, ratio: 1.0741311701837128 +total sulprus: 0.10989355299770467, spot_surplus: 0.21909203256617396, spot_price: 2200.15, frac_price: 2058.9502993257292, liquidator_profit:0.10919847956846929, 0.23208087134814293 % of position +user state after part liq: health: 0.9683299972690153, collateral: 0.014693877549975363, stablecoin: 16.533327139610034, debt: 44.69931321243116 +user state after repay: health: 1.2171730772730216, collateral: 0.014693877549975363, stablecoin: 16.533327139610034, debt: 44.58941965943345 +----------------------------- +User: 0xd5d7d4bA0f5FBCC8c2c04D14EcE01dC6e6261DC0, block: 18819857, ratio: 1.0739881487724594 +user state: health: 0.9548859846111921, collateral: 2.2064151404400474, stablecoin: 1745.8490257936862, debt: 6117.0328742734255, ratio: 1.0739881487724594 +total sulprus: 16.17079004465453, spot_surplus: 30.429240136821242, spot_price: 2256.95, frac_price: 2127.704602514892, liquidator_profit:14.258450092166711, 0.2330942204370663 % of position +user state after part liq: health: 0.9548859846111921, collateral: 2.096094383418045, stablecoin: 1658.556574504002, debt: 5811.181230559755 +user state after repay: health: 1.2365973433734854, collateral: 2.096094383418045, stablecoin: 1658.556574504002, debt: 5795.0104405151005 +----------------------------- +User: 0xD6AF98Abce0f9260Fcd2C1c49884413fcDC60F6F, block: 18816304, ratio: 1.074164038477689 +user state: health: 0.97141961690276, collateral: 8.026766127709068, stablecoin: 14819.304751650787, debt: 30097.322217798726, ratio: 1.074164038477689 +total sulprus: 56.653973761109974, spot_surplus: 127.74842860610113, spot_price: 2221.69, frac_price: 2044.546543435177, liquidator_profit:71.09445484499115, 0.23621521652497 % of position +user state after part liq: health: 0.97141961690276, collateral: 7.625427821323616, stablecoin: 14078.339514068246, debt: 28592.45610690879 +user state after repay: health: 1.1718847074198084, collateral: 7.625427821323616, stablecoin: 14078.339514068246, debt: 28535.80213314768 +----------------------------- +User: 0xd87EcC6C74F486B044824a222326A96F696fCfA2, block: 20283113, ratio: 1.0744728226204232 +user state: health: 1.0004453263197866, collateral: 5.365143753927458, stablecoin: 81831.3502364945, debt: 91338.94213874346, ratio: 1.0744728226204232 +total sulprus: 35.40286026417797, spot_surplus: 371.15692952911866, spot_price: 3155.69, frac_price: 1904.077425708182, liquidator_profit:335.75406926494065, 0.36759137056233 % of position +user state after part liq: health: 1.0004453263197866, collateral: 5.096886566231085, stablecoin: 77739.78272466979, debt: 86771.99503180629 +user state after repay: health: 1.041670195351252, collateral: 5.096886566231085, stablecoin: 77739.78272466979, debt: 86736.59217154211 +----------------------------- +User: 0xD9F4DdA53ABc0ad6eae07eD2e47b3108c3a131b8, block: 18356829, ratio: 1.073964318668985 +user state: health: 0.9526459548845871, collateral: 2.042805932000717, stablecoin: 4562.18121574103, debt: 7183.461782934506, ratio: 1.073964318668985 +total sulprus: 9.694061559635784, spot_surplus: 27.61500802034593, spot_price: 1553.54, frac_price: 1378.0857761799384, liquidator_profit:17.920946460710145, 0.2494750720785388 % of position +user state after part liq: health: 0.9526459548845871, collateral: 1.9406656354006813, stablecoin: 4334.072154953979, debt: 6824.288693787781 +user state after repay: health: 1.0962555483102177, collateral: 1.9406656354006813, stablecoin: 4334.072154953979, debt: 6814.594632228145 +----------------------------- +User: 0xE408D65d495c567aB246E7c90F11d15d96c1738D, block: 18215044, ratio: 1.0748962256817993 +user state: health: 1.0402452140891427, collateral: 5.93213222694e-07, stablecoin: 0.00046308876634323, debt: 0.001276361286092946, ratio: 1.0748962256817993 +total sulprus: 3.045552108999e-06, spot_surplus: 6.521443570678539e-06, spot_price: 1590.83, frac_price: 1473.6413963641107, liquidator_profit:3.4758914616795393e-06, 0.272328180081327 % of position +user state after part liq: health: 1.0402452140893308, collateral: 5.63552561556e-07, stablecoin: 0.000439934328026069, debt: 0.001212543221788299 +user state after repay: health: 1.2946676405394282, collateral: 5.63552561556e-07, stablecoin: 0.000439934328026069, debt: 0.0012094976696793 +----------------------------- +User: 0xe4727db0D9eF3cA11b9D177c2E92f63b512993A5, block: 20467001, ratio: 1.0743041839994478 +user state: health: 0.984593295948089, collateral: 7.6276137891172e-05, stablecoin: 0.0, debt: 0.1589970028326524, ratio: 1.0743041839994478 +total sulprus: 0.000590707127691906, spot_surplus: 0.001559953626090633, spot_price: 2493.52, frac_price: 2239.3785279235676, liquidator_profit:0.000969246498398727, 0.6096004837392306 % of position +user state after part liq: health: 0.984593295948094, collateral: 7.2462330996612e-05, stablecoin: 0.0, debt: 0.1510471526910198 +user state after repay: health: 3.5669635928665753, collateral: 7.2462330996602e-05, stablecoin: 0.0, debt: 0.15045644556332788 +----------------------------- +User: 0xe77Adf593302A6524D054A27E886021c2cEf8c0B, block: 20461028, ratio: 1.0748956433107943 +user state: health: 1.0401904712146683, collateral: 7.200488804494e-06, stablecoin: 0.020409902448740216, debt: 0.03413764195659981, ratio: 1.0748956433107943 +total sulprus: 5.1407394082207e-05, spot_surplus: 0.00016247504952511776, spot_price: 2357.79, frac_price: 2049.289678817646, liquidator_profit:0.00011106765544291075, 0.3253524528264557 % of position +user state after part liq: health: 1.0401904712146952, collateral: 6.840464364269e-06, stablecoin: 0.019389407326303208, debt: 0.03243075985876982 +user state after repay: health: 1.2006079130199232, collateral: 6.840464364269e-06, stablecoin: 0.019389407326303208, debt: 0.032379352464687615 +----------------------------- +User: 0xE9e2aa87f02e92c33b7A4C705196c9218b11d2e5, block: 19491216, ratio: 1.0744342460597456 +user state: health: 0.9968191296160922, collateral: 16.502504259364173, stablecoin: 12806.570831717274, debt: 55274.78331873692, ratio: 1.0744342460597456 +total sulprus: 158.054468898819, spot_surplus: 637.4583382406441, spot_price: 3346, frac_price: 2764.992582207888, liquidator_profit:479.40386934182504, 0.8673102643883506 % of position +user state after part liq: health: 0.9968191296160922, collateral: 15.677379046395965, stablecoin: 12166.24229013141, debt: 52511.044152800074 +user state after repay: health: 1.3017300564682837, collateral: 15.677379046395965, stablecoin: 12166.24229013141, debt: 52352.98968390126 +----------------------------- +User: 0xebdeff4D7053bF84262D2f9FC261a900c4323d83, block: 20055315, ratio: 1.074647592828792 +user state: health: 1.0168737259064424, collateral: 5.171745985844236, stablecoin: 0.0, debt: 16834.633368678493, ratio: 1.074647592828792 +total sulprus: 62.83324285635531, spot_surplus: 113.06314610767377, spot_price: 3692.35, frac_price: 3498.1026282659495, liquidator_profit:50.229903251318454, 0.29837242161016225 % of position +user state after part liq: health: 1.0168737259064424, collateral: 4.913158686552024, stablecoin: 0.0, debt: 15992.901700244565 +user state after repay: health: 3.4779468535698363, collateral: 4.913158686552024, stablecoin: 0.0, debt: 15930.068457388212 +----------------------------- +User: 0xEC3c1940d88B8875b39b41e6D023026da500D4bB, block: 19889538, ratio: 1.0744497965463782 +user state: health: 0.9982808753595556, collateral: 0.003456142295976889, stablecoin: 2.600732533370236, debt: 11.711151629194951, ratio: 1.0744497965463782 +total sulprus: 0.033913442406819434, spot_surplus: 0.06875336658353312, spot_price: 3033.87, frac_price: 2832.258369499317, liquidator_profit:0.03483992417671369, 0.29749357945174737 % of position +user state after part liq: health: 0.9982808753595557, collateral: 0.003283335181178045, stablecoin: 2.470695906701724, debt: 11.125594047735202 +user state after repay: health: 1.307088846264238, collateral: 0.003283335181178045, stablecoin: 2.470695906701724, debt: 11.091680605328383 +----------------------------- +User: 0xf11F0Add0e8E4ee208104d8264fcf1B69C4CeAfc, block: 18817492, ratio: 1.0745941862637796 +user state: health: 1.011853508795285, collateral: 11.999210340874825, stablecoin: 6002.823225056494, debt: 30094.93878795306, ratio: 1.0745941862637796 +total sulprus: 89.85658778936045, spot_surplus: 141.78961657371323, spot_price: 2244.14, frac_price: 2157.5792559026236, liquidator_profit:51.93302878435277, 0.1725639953956027 % of position +user state after part liq: health: 1.011853508795285, collateral: 11.399249823831083, stablecoin: 5702.68206380367, debt: 28590.191848555405 +user state after repay: health: 1.3303262705902672, collateral: 11.399249823831083, stablecoin: 5702.68206380367, debt: 28500.335260766045 +----------------------------- +User: 0xF9605D8c4c987d7Cb32D0d11FbCb8EeeB1B22D5d, block: 18204326, ratio: 1.0745939224615029 +user state: health: 1.0118287113812716, collateral: 0.001023456372886911, stablecoin: 0.3634442431739745, debt: 1.73760743338035, ratio: 1.0745939224615029 +total sulprus: 0.00512521112298529, spot_surplus: 0.0127748313452614, spot_price: 1592.31, frac_price: 1442.823995028469, liquidator_profit:0.00764962022227611, 0.44023869116365955 % of position +user state after part liq: health: 1.0118287113812716, collateral: 0.000972283554242564, stablecoin: 0.34527203101527576, debt: 1.6507270617113325 +user state after repay: health: 1.3264290795563327, collateral: 0.000972283554242564, stablecoin: 0.34527203101527576, debt: 1.6456018505883472 +----------------------------- +User: 0xfBB4D0C7282E3cfB1F1243345F245188b17cC2Fd, block: 20480137, ratio: 1.0749011203232466 +user state: health: 1.0407053103851738, collateral: 0.000250457422369141, stablecoin: 1.8219862568280978, debt: 2.2394553048287222, ratio: 1.0749011203232466 +total sulprus: 0.001563444969776299, spot_surplus: 0.008557047930299308, spot_price: 2350.14, frac_price: 1791.673583283837, liquidator_profit:0.006993602960523009, 0.3122903567418102 % of position +user state after part liq: health: 1.040705310385174, collateral: 0.000237934551250684, stablecoin: 1.730886943986693, debt: 2.127482539587286 +user state after repay: health: 1.1150127395155345, collateral: 0.000237934551250684, stablecoin: 1.730886943986693, debt: 2.12591909461751 +----------------------------- +User: 0xfea6e4c830210361aBA38A07828B73686643f411, block: 21379079, ratio: 1.074746859694929 +user state: health: 1.0262048113233195, collateral: 10.616183238564666, stablecoin: 12833.69387607899, debt: 47192.084799123986, ratio: 1.074746859694929 +total sulprus: 128.40909128341832, spot_surplus: 253.35174659224896, spot_price: 3713.71, frac_price: 3478.328502712048, liquidator_profit:124.94265530883062, 0.26475341329092944 % of position +user state after part liq: health: 1.0262048113233195, collateral: 10.08537407663643, stablecoin: 12192.00918227504, debt: 44832.48055916779 +user state after repay: health: 1.3163950049604736, collateral: 10.08537407663643, stablecoin: 12192.00918227504, debt: 44704.071467884365 +----------------------------- diff --git a/tests/zaps/partial_liquidation/conftest.py b/tests/zaps/partial_liquidation/conftest.py new file mode 100644 index 00000000..a8fb8ecb --- /dev/null +++ b/tests/zaps/partial_liquidation/conftest.py @@ -0,0 +1,70 @@ +import boa +import pytest + +from tests.utils.deployers import ( + PARTIAL_REPAY_ZAP_DEPLOYER, + PARTIAL_REPAY_ZAP_CALLBACK_DEPLOYER, +) + + +@pytest.fixture(scope="module") +def partial_repay_zap(admin): + with boa.env.prank(admin): + return PARTIAL_REPAY_ZAP_DEPLOYER.deploy(5 * 10**16, 1 * 10**16) + + +@pytest.fixture(scope="module") +def partial_repay_zap_callback(admin): + with boa.env.prank(admin): + return PARTIAL_REPAY_ZAP_CALLBACK_DEPLOYER.deploy(5 * 10**16, 1 * 10**16) + + +@pytest.fixture(scope="module") +def partial_repay_zap_tester(admin): + with boa.env.prank(admin): + return boa.load("contracts/testing/zaps/PartialRepayZapTester.vy") + + +@pytest.fixture(scope="module") +def controller_for_liquidation( + borrowed_token, + collateral_token, + controller, + amm, + monetary_policy, + admin, +): + def f(sleep_time, user): + N = 5 + collateral_amount = 10**18 + + with boa.env.prank(admin): + controller.set_amm_fee(10**6) + monetary_policy.set_rate(int(1e18 * 1.0 / 365 / 86400)) # 100% APY + + debt = controller.max_borrowable(collateral_amount, N) + with boa.env.prank(user): + boa.deal(collateral_token, user, collateral_amount) + borrowed_token.approve(amm, 2**256 - 1) + borrowed_token.approve(controller, 2**256 - 1) + collateral_token.approve(controller, 2**256 - 1) + controller.create_loan(collateral_amount, debt, N) + + health_0 = controller.health(user) + with boa.env.prank(user): + amm.exchange(0, 1, debt, 0) + health_1 = controller.health(user) + + assert health_0 <= health_1 + + boa.env.time_travel(sleep_time) + + health_2 = controller.health(user) + assert 0 < health_2 < controller.liquidation_discount() + + with boa.env.prank(admin): + monetary_policy.set_rate(0) + + return controller + + return f diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap.py b/tests/zaps/partial_liquidation/test_partial_repay_zap.py new file mode 100644 index 00000000..f913bfd5 --- /dev/null +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap.py @@ -0,0 +1,50 @@ +import boa +import pytest + + +@pytest.mark.parametrize("is_approved", [True, False]) +def test_users_to_liquidate( + controller_for_liquidation, + accounts, + partial_repay_zap, + is_approved, +): + user = accounts[1] + controller = controller_for_liquidation(sleep_time=int(33 * 86400), user=user) + + if is_approved: + someone_else = str(partial_repay_zap.address) + controller.approve(someone_else, True, sender=user) + + users_to_liquidate = partial_repay_zap.users_to_liquidate(controller.address) + + if not is_approved: + assert users_to_liquidate == [] + else: + assert len(users_to_liquidate) == 1 + assert users_to_liquidate[0][0] == user + + +def test_liquidate_partial( + borrowed_token, + controller_for_liquidation, + accounts, + partial_repay_zap, +): + user = accounts[1] + liquidator = accounts[2] + controller = controller_for_liquidation(sleep_time=int(30.7 * 86400), user=user) + someone_else = str(partial_repay_zap.address) + controller.approve(someone_else, True, sender=user) + + h = controller.health(user) / 10**16 + assert 0.9 < h < 1 + + # Ensure liquidator has stablecoin + boa.deal(borrowed_token, liquidator, 10**21) + with boa.env.prank(liquidator): + borrowed_token.approve(partial_repay_zap.address, 2**256 - 1) + partial_repay_zap.liquidate_partial(controller.address, user, 0) + + h = controller.health(user) / 10**16 + assert h > 1 diff --git a/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py new file mode 100644 index 00000000..e42b4d8b --- /dev/null +++ b/tests/zaps/partial_liquidation/test_partial_repay_zap_callback.py @@ -0,0 +1,134 @@ +import boa +import pytest + +from eth_utils import to_bytes + +from tests.utils.constants import MAX_UINT256 + + +@pytest.mark.parametrize("is_approved", [True, False]) +def test_users_to_liquidate_callback( + controller_for_liquidation, + accounts, + partial_repay_zap_callback, + is_approved, +): + user = accounts[1] + controller = controller_for_liquidation(sleep_time=int(33 * 86400), user=user) + + if is_approved: + controller.approve(partial_repay_zap_callback.address, True, sender=user) + + users_to_liquidate = partial_repay_zap_callback.users_to_liquidate( + controller.address + ) + + if not is_approved: + assert users_to_liquidate == [] + else: + assert len(users_to_liquidate) == 1 + assert users_to_liquidate[0][0] == user + + +def test_liquidate_partial( + borrowed_token, + controller_for_liquidation, + accounts, + partial_repay_zap_callback, +): + user = accounts[1] + liquidator = accounts[2] + controller = controller_for_liquidation(sleep_time=int(30.7 * 86400), user=user) + someone_else = str(partial_repay_zap_callback.address) + controller.approve(someone_else, True, sender=user) + + h = controller.health(user) / 10**16 + assert 0.9 < h < 1 + + # Ensure liquidator has stablecoin + boa.deal(borrowed_token, liquidator, 10**21) + with boa.env.prank(liquidator): + borrowed_token.approve(partial_repay_zap_callback.address, 2**256 - 1) + partial_repay_zap_callback.liquidate_partial(controller.address, user, 0) + + h = controller.health(user) / 10**16 + assert h > 1 + + +def test_liquidate_partial_callback( + borrowed_token, + collateral_token, + controller_for_liquidation, + accounts, + partial_repay_zap_callback, + partial_repay_zap_tester, +): + calldata = to_bytes(hexstr=borrowed_token.address) + + user = accounts[1] + liquidator = accounts[2] + controller = controller_for_liquidation(sleep_time=int(30.7 * 86400), user=user) + controller.approve(partial_repay_zap_callback.address, True, sender=user) + + initial_health = controller.health(user) + + initial_collateral = collateral_token.balanceOf(partial_repay_zap_tester.address) + # Ensure partial_repay_zap_tester has stablecoin + boa.deal(borrowed_token, partial_repay_zap_tester, 10**21) + with boa.env.prank(liquidator): + borrowed_token.approve(partial_repay_zap_callback.address, 2**256 - 1) + partial_repay_zap_callback.liquidate_partial( + controller.address, user, 0, partial_repay_zap_tester.address, calldata + ) + + final_health = controller.health(user) + assert final_health > initial_health + + final_collateral = collateral_token.balanceOf(partial_repay_zap_tester.address) + assert final_collateral > initial_collateral + + assert borrowed_token.balanceOf(partial_repay_zap_callback.address) == 0 + assert collateral_token.balanceOf(partial_repay_zap_callback.address) == 0 + + +@pytest.mark.parametrize("use_callback", [True, False]) +def test_liquidate_partial_uses_exact_amount( + accounts, + borrowed_token, + controller_for_liquidation, + partial_repay_zap_callback, + partial_repay_zap_tester, + use_callback, +): + user = accounts[1] + liquidator = accounts[2] + controller = controller_for_liquidation(sleep_time=int(30.7 * 86400), user=user) + controller.approve(partial_repay_zap_callback.address, True, sender=user) + + position = partial_repay_zap_callback.users_to_liquidate(controller.address)[0] + borrowed_from_sender = position.dy + + if use_callback: + calldata = to_bytes(hexstr=borrowed_token.address) + boa.deal(borrowed_token, partial_repay_zap_tester, 10**21) + pre_balance = borrowed_token.balanceOf(partial_repay_zap_tester) + + with boa.env.prank(liquidator): + borrowed_token.approve(partial_repay_zap_callback.address, 2**256 - 1) + partial_repay_zap_callback.liquidate_partial( + controller.address, user, 0, partial_repay_zap_tester.address, calldata + ) + + post_balance = borrowed_token.balanceOf(partial_repay_zap_tester) + + else: + # get money to liquidate: in real scenario this would be a separate address + boa.deal(borrowed_token, liquidator, 10**21) + pre_balance = borrowed_token.balanceOf(liquidator) + with boa.env.prank(liquidator): + borrowed_token.approve(partial_repay_zap_callback.address, MAX_UINT256) + partial_repay_zap_callback.liquidate_partial(controller.address, user, 0) + post_balance = borrowed_token.balanceOf(liquidator) + + spent = pre_balance - post_balance + assert spent == borrowed_from_sender diff --git a/tests_brownie/stablecoin/conftest.py b/tests_brownie/stablecoin/conftest.py deleted file mode 100644 index bf5407b4..00000000 --- a/tests_brownie/stablecoin/conftest.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest - - -@pytest.fixture(scope="session") -def alice(accounts): - yield accounts[0] - - -@pytest.fixture(scope="session") -def bob(accounts): - yield accounts[1] - - -@pytest.fixture(scope="session") -def charlie(accounts): - yield accounts[2] - - -@pytest.fixture(scope="module") -def stablecoin(alice, Stablecoin): - yield Stablecoin.deploy("CurveFi USD Stablecoin", "crvUSD", {"from": alice}) - - -@pytest.fixture(autouse=True) -def isolate(fn_isolation): - ... diff --git a/tests_brownie/stablecoin/test_admin.py b/tests_brownie/stablecoin/test_admin.py deleted file mode 100644 index bd3f5971..00000000 --- a/tests_brownie/stablecoin/test_admin.py +++ /dev/null @@ -1,38 +0,0 @@ -import brownie - - -def test_add_minter(alice, bob, stablecoin): - tx = stablecoin.add_minter(bob, {"from": alice}) - - assert stablecoin.is_minter(bob) is True - assert tx.events["AddMinter"].values() == [bob] - - -def test_add_minter_reverts_invalid_caller(bob, stablecoin): - with brownie.reverts(): - stablecoin.add_minter(bob, {"from": bob}) - - -def test_remove_minter(alice, bob, stablecoin): - stablecoin.add_minter(bob, {"from": alice}) - tx = stablecoin.remove_minter(bob, {"from": alice}) - - assert stablecoin.is_minter(bob) is False - assert tx.events["RemoveMinter"] == [bob] - - -def test_remove_minter_reverts_invalid_caller(bob, stablecoin): - with brownie.reverts(): - stablecoin.remove_minter(bob, {"from": bob}) - - -def test_set_admin(alice, bob, stablecoin): - tx = stablecoin.set_admin(bob, {"from": alice}) - - assert stablecoin.admin() == bob - assert tx.events["SetAdmin"].values() == [bob] - - -def test_set_admin_reverts_invalid_caller(bob, stablecoin): - with brownie.reverts(): - stablecoin.set_admin(bob, {"from": bob}) diff --git a/tests_brownie/stablecoin/test_approve.py b/tests_brownie/stablecoin/test_approve.py deleted file mode 100644 index d82b1d09..00000000 --- a/tests_brownie/stablecoin/test_approve.py +++ /dev/null @@ -1,172 +0,0 @@ -from copy import deepcopy - -import boa -import brownie -from brownie import ZERO_ADDRESS, accounts -from eth_account import Account as EthAccount -from eth_account._utils.structured_data.hashing import hash_domain, hash_message -from eth_account.messages import SignableMessage - -AMOUNT = 10**21 - -PERMIT_STRUCT = { - "types": { - "EIP712Domain": [ - {"name": "name", "type": "string"}, - {"name": "version", "type": "string"}, - {"name": "chainId", "type": "uint256"}, - {"name": "verifyingContract", "type": "address"}, - {"name": "salt", "type": "bytes32"}, - ], - "Permit": [ - {"name": "owner", "type": "address"}, - {"name": "spender", "type": "address"}, - {"name": "value", "type": "uint256"}, - {"name": "nonce", "type": "uint256"}, - {"name": "deadline", "type": "uint256"}, - ], - }, - "primaryType": "Permit", -} - - -def test_approve(alice, bob, stablecoin): - tx = stablecoin.approve(bob, AMOUNT, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == AMOUNT - assert tx.events["Approval"].values() == [alice, bob, AMOUNT] - assert tx.return_value is True - - -def test_nonzero_to_nonzero_approval(alice, bob, stablecoin): - stablecoin.approve(bob, 20, {"from": alice}) - tx = stablecoin.approve(bob, AMOUNT, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == AMOUNT - assert tx.events["Approval"].values() == [alice, bob, AMOUNT] - assert tx.return_value is True - - -def test_increaseAllowance(alice, bob, stablecoin): - tx = stablecoin.increaseAllowance(bob, AMOUNT, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == AMOUNT - assert tx.events["Approval"].values() == [alice, bob, AMOUNT] - assert tx.return_value is True - - -def test_increaseAllowance_does_not_overflow(alice, bob, stablecoin): - for _ in range(10): - stablecoin.increaseAllowance(bob, 2**255 - 1, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == 2**256 - 1 - - tx = stablecoin.increaseAllowance(bob, 10, {"from": alice}) - assert "Approval" not in tx.events - assert tx.return_value is True - - -def test_decreaseAllowance(alice, bob, stablecoin): - stablecoin.approve(bob, AMOUNT, {"from": alice}) - tx = stablecoin.decreaseAllowance(bob, AMOUNT, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == 0 - assert tx.events["Approval"].values() == [alice, bob, 0] - assert tx.return_value is True - - -def test_decreaseAllowance_does_not_underflow(alice, bob, stablecoin): - for _ in range(10): - stablecoin.decreaseAllowance(bob, 10, {"from": alice}) - - assert stablecoin.allowance(alice, bob) == 0 - - tx = stablecoin.decreaseAllowance(bob, 10, {"from": alice}) - - assert "Approval" not in tx.events - assert tx.return_value is True - - -def test_permit_success(stablecoin, chain): - alice = accounts.add( - "0x416b8a7d9290502f5661da81f0cf43893e3d19cb9aea3c426cfb36e8186e9c09" - ) - - struct = deepcopy(PERMIT_STRUCT) - struct["domain"] = dict( - name=stablecoin.name(), - version=stablecoin.version(), - chainId=chain.id, - verifyingContract=stablecoin.address, - salt=stablecoin.salt(), - ) - deadline = chain.time() + 600 - struct["message"] = dict( - owner=alice.address, - spender=alice.address, - value=2**256 - 1, - nonce=stablecoin.nonces(alice), - deadline=deadline, - ) - signable_message = SignableMessage( - b"\x01", hash_domain(struct), hash_message(struct) - ) - sig = EthAccount.sign_message(signable_message, alice.private_key) - - tx = stablecoin.permit(alice, alice, 2**256 - 1, deadline, sig.v, sig.r, sig.s) - - assert stablecoin.nonces(alice) == 1 - assert stablecoin.allowance(alice, alice) == 2**256 - 1 - assert tx.events["Approval"].values() == [alice, alice, 2**256 - 1] - assert tx.return_value is True - - -def test_permit_reverts_owner_is_invalid(bob, chain, stablecoin): - with brownie.reverts(): - stablecoin.permit( - ZERO_ADDRESS, - bob, - 2**256 - 1, - chain.time() + 600, - 27, - b"\x00" * 32, - b"\x00" * 32, - {"from": bob}, - ) - - -def test_permit_reverts_deadline_is_invalid(bob, chain, stablecoin): - with brownie.reverts(): - stablecoin.permit( - bob, - bob, - 2**256 - 1, - chain.time() - 600, - 27, - b"\x00" * 32, - b"\x00" * 32, - {"from": bob}, - ) - - -def test_permit_reverts_signature_is_invalid(bob, chain, stablecoin): - with brownie.reverts(): - stablecoin.permit( - bob, - bob, - 2**256 - 1, - chain.time() + 600, - 27, - b"\x00" * 32, - b"\x00" * 32, - {"from": bob}, - ) - - -def test_domain_separator_updates_when_chain_id_updates(): - stablecoin = boa.load("contracts/Stablecoin.vy", "CurveFi USD Stablecoin", "crvUSD") - - domain_separator = stablecoin.DOMAIN_SEPARATOR() - with boa.env.anchor(): - boa.env.vm.patch.chain_id = 42 - assert domain_separator != stablecoin.DOMAIN_SEPARATOR() diff --git a/tests_brownie/stablecoin/test_burn.py b/tests_brownie/stablecoin/test_burn.py deleted file mode 100644 index 6afbf286..00000000 --- a/tests_brownie/stablecoin/test_burn.py +++ /dev/null @@ -1,62 +0,0 @@ -import brownie -import pytest -from brownie import ZERO_ADDRESS - -AMOUNT = 10**21 - - -@pytest.fixture(autouse=True) -def mint(alice, bob, stablecoin): - stablecoin.mint(alice, AMOUNT, {"from": alice}) - - -def test_burn(alice, stablecoin): - tx = stablecoin.burn(AMOUNT, {"from": alice}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.totalSupply() == 0 - assert tx.events["Transfer"].values() == [alice, ZERO_ADDRESS, AMOUNT] - assert tx.return_value is True - - -def test_burn_reverts_insufficient_balance(alice, stablecoin): - with brownie.reverts(): - stablecoin.burn(AMOUNT + 1, {"from": alice}) - - -def test_burnFrom(alice, bob, stablecoin): - stablecoin.approve(bob, AMOUNT, {"from": alice}) - tx = stablecoin.burnFrom(alice, AMOUNT, {"from": bob}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.totalSupply() == 0 - assert tx.events["Transfer"].values() == [alice, ZERO_ADDRESS, AMOUNT] - - assert stablecoin.allowance(alice, bob) == 0 - assert tx.events["Approval"].values() == [alice, bob, 0] - assert tx.return_value is True - - -def test_burnFrom_with_infinite_allowance(alice, bob, stablecoin): - stablecoin.approve(bob, 2**256 - 1, {"from": alice}) - tx = stablecoin.burnFrom(alice, AMOUNT, {"from": bob}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.totalSupply() == 0 - assert tx.events["Transfer"].values() == [alice, ZERO_ADDRESS, AMOUNT] - - assert stablecoin.allowance(alice, bob) == 2**256 - 1 - assert "Approval" not in tx.events - assert tx.return_value is True - - -def test_burnFrom_reverts_insufficient_allowance(alice, bob, stablecoin): - with brownie.reverts(): - stablecoin.burnFrom(alice, AMOUNT, {"from": bob}) - - -def test_burnFrom_reverts_insufficient_balance(alice, bob, stablecoin): - stablecoin.approve(bob, 2**256 - 1, {"from": alice}) - - with brownie.reverts(): - stablecoin.burnFrom(alice, AMOUNT + 1, {"from": bob}) diff --git a/tests_brownie/stablecoin/test_mint.py b/tests_brownie/stablecoin/test_mint.py deleted file mode 100644 index 79f63dce..00000000 --- a/tests_brownie/stablecoin/test_mint.py +++ /dev/null @@ -1,24 +0,0 @@ -import brownie -import pytest -from brownie import ZERO_ADDRESS - - -def test_mint(alice, bob, stablecoin): - tx = stablecoin.mint(bob, 10**18, {"from": alice}) - - assert stablecoin.balanceOf(bob) == 10**18 - assert stablecoin.totalSupply() == 10**18 - assert tx.events["Transfer"].values() == [ZERO_ADDRESS, bob, 10**18] - assert tx.return_value is True - - -def test_mint_reverts_caller_is_invalid(bob, stablecoin): - with brownie.reverts(): - stablecoin.mint(bob, 10**18, {"from": bob}) - - -@pytest.mark.parametrize("idx", [0, 1]) -def test_mint_reverts_receiver_is_invalid(alice, stablecoin, idx): - receiver = [ZERO_ADDRESS, stablecoin][idx] - with brownie.reverts(): - stablecoin.mint(receiver, 10**18, {"from": alice}) diff --git a/tests_brownie/stablecoin/test_transfer.py b/tests_brownie/stablecoin/test_transfer.py deleted file mode 100644 index bbcac607..00000000 --- a/tests_brownie/stablecoin/test_transfer.py +++ /dev/null @@ -1,79 +0,0 @@ -import brownie -import pytest -from brownie import ZERO_ADDRESS - -AMOUNT = 10**21 - - -@pytest.fixture(autouse=True) -def mint(alice, stablecoin): - stablecoin.mint(alice, AMOUNT, {"from": alice}) - - -def test_transfer(alice, bob, stablecoin): - tx = stablecoin.transfer(bob, AMOUNT, {"from": alice}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.balanceOf(bob) == AMOUNT - assert stablecoin.totalSupply() == AMOUNT # unchanged - assert tx.events["Transfer"].values() == [alice, bob, AMOUNT] - assert tx.return_value is True - - -@pytest.mark.parametrize("idx", [0, 1]) -def test_transfer_reverts_invalid_receipient(alice, stablecoin, idx): - receiver = [ZERO_ADDRESS, stablecoin][idx] - - with brownie.reverts(): - stablecoin.transfer(receiver, AMOUNT, {"from": alice}) - - -def test_transfer_reverts_insufficient_balance(alice, bob, stablecoin): - with brownie.reverts(): - stablecoin.transfer(bob, AMOUNT + 1, {"from": alice}) - - -def test_transferFrom(alice, bob, stablecoin): - stablecoin.approve(bob, AMOUNT, {"from": alice}) - tx = stablecoin.transferFrom(alice, bob, AMOUNT, {"from": bob}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.balanceOf(bob) == AMOUNT - assert tx.events["Transfer"].values() == [alice, bob, AMOUNT] - - assert stablecoin.allowance(alice, bob) == 0 - assert tx.events["Approval"].values() == [alice, bob, 0] - assert tx.return_value is True - - -def test_transferFrom_with_infinite_allowance(alice, bob, stablecoin): - stablecoin.approve(bob, 2**256 - 1, {"from": alice}) - tx = stablecoin.transferFrom(alice, bob, AMOUNT, {"from": bob}) - - assert stablecoin.balanceOf(alice) == 0 - assert stablecoin.balanceOf(bob) == AMOUNT - assert tx.events["Transfer"].values() == [alice, bob, AMOUNT] - - assert stablecoin.allowance(alice, bob) == 2**256 - 1 - assert "Approval" not in tx.events - assert tx.return_value is True - - -@pytest.mark.parametrize("idx", [0, 1]) -def test_transferFrom_reverts_invalid_receipient(alice, bob, stablecoin, idx): - receiver = [ZERO_ADDRESS, stablecoin][idx] - stablecoin.approve(bob, 2**256 - 1, {"from": alice}) - - with brownie.reverts(): - stablecoin.transferFrom(alice, receiver, AMOUNT, {"from": bob}) - - -def test_transferFrom_reverts_insufficient_allowance(alice, bob, stablecoin): - with brownie.reverts(): - stablecoin.transferFrom(alice, bob, AMOUNT, {"from": bob}) - - -def test_transferFrom_reverts_insufficient_balance(alice, bob, stablecoin): - stablecoin.approve(bob, 2**256 - 1, {"from": alice}) - with brownie.reverts(): - stablecoin.transferFrom(alice, bob, AMOUNT + 1, {"from": bob}) diff --git a/tests_forked/price_oracles/conftest.py b/tests_forked/price_oracles/conftest.py deleted file mode 100644 index ca4fbcce..00000000 --- a/tests_forked/price_oracles/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -import boa -import pytest -from .settings import WEB3_PROVIDER_URL, EXPLORER_URL, EXPLORER_TOKEN - - -@pytest.fixture(scope="module", autouse=True) -def boa_fork(): - assert WEB3_PROVIDER_URL is not None, "Provider url is not set, add WEB3_PROVIDER_URL param to env" - boa.fork(WEB3_PROVIDER_URL) - - -@pytest.fixture(scope="module") -def stablecoin_aggregator(): - return boa.from_etherscan("0x18672b1b0c623a30089A280Ed9256379fb0E4E62", "AggregatorStablePrice", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) # USD/crvUSD diff --git a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py b/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py deleted file mode 100644 index 9accc4f5..00000000 --- a/tests_forked/price_oracles/test_lp_oracle_compare_to_spot.py +++ /dev/null @@ -1,276 +0,0 @@ -import boa -from .settings import EXPLORER_URL, EXPLORER_TOKEN - - -def test_tricrypto_usdc(stablecoin_aggregator): - tricrypto_usdc_pool_address = "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B" - crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" - - tricrypto_usdc_pool = boa.from_etherscan(tricrypto_usdc_pool_address, "TricryptoUSDC", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - crvusd_usdc_pool = boa.from_etherscan(crvusd_usdc_pool_address, "crvUSD/USDC", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - usdc_crvusd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRate.vy', [crvusd_usdc_pool_address], [1], [0]) # crvUSD/USDC - usdc_usd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy', [crvusd_usdc_pool_address], [1], [0], stablecoin_aggregator.address) # USD/USDC - tricrypto_usdc_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrypto_usdc_pool_address, usdc_crvusd_oracle.address) # USDC/LP * crvUSD/USDC - tricrypto_usdc_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrypto_usdc_pool_address, usdc_usd_oracle.address) # USDC/LP * crvUSD/USDC * USD/crvUSD - - # --- Compare current oracle and spot prices --- - - # Oracle price - lp_oracle_price_crvusd = tricrypto_usdc_crvusd_lp_oracle.price() - lp_oracle_price_usd = tricrypto_usdc_usd_lp_oracle.price() - - # Spot price - usdc_from_lp = tricrypto_usdc_pool.calc_withdraw_one_coin(10 ** 18, 0) - crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10 ** 18 - - print("LP/crvUSD spot price:", lp_spot_price_crvusd / 10**18, "LP/crvUSD oracle price:", lp_oracle_price_crvusd / 10**18) - print("LP/USD spot price:", lp_spot_price_usd / 10**18, "LP/USD oracle price:", lp_oracle_price_usd / 10**18) - print("WBTC/USDC spot price:", tricrypto_usdc_pool.get_dy(1, 0, 10**4) / 10**2, "WBTC/USDC oracle price:", tricrypto_usdc_pool.price_oracle(0) / 10**18) - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 - - boa.env.time_travel(3600) - - # --- DUMP & PUMP --- - - trader = boa.env.generate_address() - erc_mock = boa.load_partial("contracts/testing/ERC20Mock.vy") - usdc = erc_mock.at(tricrypto_usdc_pool.coins(0)) - wbtc = erc_mock.at(tricrypto_usdc_pool.coins(1)) - boa.deal(wbtc, trader, 1001 * 10**8) - initial_wbtc_spot_price = tricrypto_usdc_pool.get_dy(1, 0, 10**4) - print("USDC pool balance:", usdc.balanceOf(tricrypto_usdc_pool) / 10**6, ", WBTC pool balance:", wbtc.balanceOf(tricrypto_usdc_pool) / 10**8) - with boa.env.prank(trader): - - # --- DUMP --- - - print("\nTrade 1000 BTC -> POOL -> X USDC") - wbtc.approve(tricrypto_usdc_pool, 2**256-1) - tricrypto_usdc_pool.exchange(1, 0, 1000 * 10**8, 0) - - # Align oracle - for i in range(100): - boa.env.time_travel(3600) - tricrypto_usdc_pool.exchange(1, 0, 100, 0) - - # Oracle price - lp_oracle_price_crvusd = tricrypto_usdc_crvusd_lp_oracle.price_w() - lp_oracle_price_usd = tricrypto_usdc_usd_lp_oracle.price_w() - - # Spot price - usdc_from_lp = tricrypto_usdc_pool.calc_withdraw_one_coin(10 ** 18, 0) - crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10 ** 18 - - print("LP/crvUSD spot price:", lp_spot_price_crvusd / 10 ** 18, "LP/crvUSD oracle price:", lp_oracle_price_crvusd / 10 ** 18) - print("LP/USD spot price:", lp_spot_price_usd / 10 ** 18, "LP/USD oracle price:", lp_oracle_price_usd / 10 ** 18) - print("WBTC/USDC spot price:", tricrypto_usdc_pool.get_dy(1, 0, 10 ** 4) / 10 ** 2, "WBTC/USDC oracle price:", tricrypto_usdc_pool.price_oracle(0) / 10 ** 18) - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.0005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.0005 - - # --- PUMP --- - - print("\ntrade X USDC back -> POOL -> less than 1000 BTC + arbitrage trades") - usdc.approve(tricrypto_usdc_pool, 2 ** 256 - 1) - tricrypto_usdc_pool.exchange(0, 1, usdc.balanceOf(trader), 0) - - boa.deal(usdc, trader, 1_000_000 * 10 ** 6) - while tricrypto_usdc_pool.get_dy(1, 0, 10**4) < initial_wbtc_spot_price * 9999 // 10000: - tricrypto_usdc_pool.exchange(0, 1, 500 * 10**6, 0) - boa.env.time_travel(3600) - - print("USDC pool balance:", usdc.balanceOf(tricrypto_usdc_pool) / 10**6, ", WBTC pool balance:", wbtc.balanceOf(tricrypto_usdc_pool) / 10**8) - - - # Oracle price - lp_oracle_price_crvusd = tricrypto_usdc_crvusd_lp_oracle.price() - lp_oracle_price_usd = tricrypto_usdc_usd_lp_oracle.price() - - # Spot price - usdc_from_lp = tricrypto_usdc_pool.calc_withdraw_one_coin(10**18, 0) - crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - print("LP/crvUSD spot price:", lp_spot_price_crvusd / 10**18, "LP/crvUSD oracle price:", lp_oracle_price_crvusd / 10**18) - print("LP/USD spot price:", lp_spot_price_usd / 10**18, "LP/USD oracle price:", lp_oracle_price_usd / 10**18) - print("WBTC/USDC spot price:", tricrypto_usdc_pool.get_dy(1, 0, 10**4) / 10**2, "WBTC/USDC oracle price:", tricrypto_usdc_pool.price_oracle(0) / 10**18) - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.0005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.0005 - - raise Exception("Success") - - -def test_tricrypto_usdt(stablecoin_aggregator): - tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" - crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" - - tricrypto_usdt_pool = boa.from_etherscan(tricrypto_usdt_pool_address, "TricryptoUSDT", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - crvusd_usdt_pool = boa.from_etherscan(crvusd_usdt_pool_address, "crvUSD/USDT", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - usdt_crvusd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRate.vy', [crvusd_usdt_pool_address], [1], [0]) # crvUSD/USDT - usdt_usd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy', [crvusd_usdt_pool_address], [1], [0], stablecoin_aggregator.address) # USD/USDT - tricrypto_usdt_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrypto_usdt_pool_address, usdt_crvusd_oracle.address) # USDT/LP * crvUSD/USDT - tricrypto_usdt_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrypto_usdt_pool_address, usdt_usd_oracle.address) # USDT/LP * crvUSD/USDT * USD/crvUSD - - # Oracle price - lp_oracle_price_crvusd = tricrypto_usdt_crvusd_lp_oracle.price() - lp_oracle_price_usd = tricrypto_usdt_usd_lp_oracle.price() - - # Spot price - usdt_from_lp = tricrypto_usdt_pool.calc_withdraw_one_coin(10**18, 0) - crvusd_from_lp = crvusd_usdt_pool.get_dy(0, 1, usdt_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 - - -def test_tricrv(stablecoin_aggregator): - tricrv_pool_address = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" - - tricrypto_usdt_pool = boa.from_etherscan(tricrv_pool_address, "TriCRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - tricrv_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrv_pool_address, "0x0000000000000000000000000000000000000000") # USDT/LP * crvUSD/USDT - tricrv_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', tricrv_pool_address, stablecoin_aggregator.address) # USDT/LP * crvUSD/USDT * USD/crvUSD - - # Oracle price - lp_oracle_price_crvusd = tricrv_crvusd_lp_oracle.price() - lp_oracle_price_usd = tricrv_usd_lp_oracle.price() - - # Spot price - crvusd_from_lp = tricrypto_usdt_pool.calc_withdraw_one_coin(10**18, 0) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 - - -def test_strategic_reserve(stablecoin_aggregator): - strategic_reserve_pool_address = "0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85" - crvusd_usdc_pool_address = "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E" - - strategic_reserve_pool = boa.from_etherscan(strategic_reserve_pool_address, "StrategicReserveUSD", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - crvusd_usdc_pool = boa.from_etherscan(crvusd_usdc_pool_address, "crvUSD/USDC", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - usdc_crvusd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRate.vy', [crvusd_usdc_pool_address], [1], [0]) # crvUSD/USDC - usdc_usd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy', [crvusd_usdc_pool_address], [1], [0], stablecoin_aggregator.address) # USD/USDC - strategic_reserve_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', strategic_reserve_pool_address, usdc_crvusd_oracle.address) # USDC/LP * crvUSD/USDC - strategic_reserve_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', strategic_reserve_pool_address, usdc_usd_oracle.address) # USDC/LP * crvUSD/USDC * USD/crvUSD - - # Oracle price - lp_oracle_price_crvusd = strategic_reserve_crvusd_lp_oracle.price() - lp_oracle_price_usd = strategic_reserve_usd_lp_oracle.price() - - # Spot price - usdc_from_lp = strategic_reserve_pool.calc_withdraw_one_coin(10**18, 0) - crvusd_from_lp = crvusd_usdc_pool.get_dy(0, 1, usdc_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.005 - - -def test_weeth_weth(stablecoin_aggregator): - weeth_ng_pool_address = "0xDB74dfDD3BB46bE8Ce6C33dC9D82777BCFc3dEd5" - tricrypto_usdt_pool_address = "0xf5f5B97624542D72A9E06f04804Bf81baA15e2B4" - crvusd_usdt_pool_address = "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4" - - weeth_ng_pool = boa.from_etherscan(weeth_ng_pool_address, "weETH-ng", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - tricrypto_usdt_pool = boa.from_etherscan(tricrypto_usdt_pool_address, "TricryptoUSDT", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - crvusd_usdt_pool = boa.from_etherscan(crvusd_usdt_pool_address, "crvUSD/USDT", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - usdt_crvusd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRate.vy', - [tricrypto_usdt_pool_address, crvusd_usdt_pool_address], [0, 1], [2, 0]) # crvUSD/ETH - usdt_usd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy', - [tricrypto_usdt_pool_address, crvusd_usdt_pool_address], [0, 1], [2, 0], stablecoin_aggregator.address) # USD/ETH - tricrypto_usdt_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', weeth_ng_pool_address, usdt_crvusd_oracle.address) # ETH/LP * crvUSD/ETH - tricrypto_usdt_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', weeth_ng_pool_address, usdt_usd_oracle.address) # ETH/LP * crvUSD/ETH * USD/crvUSD - - # Oracle price - lp_oracle_price_crvusd = tricrypto_usdt_crvusd_lp_oracle.price() - lp_oracle_price_usd = tricrypto_usdt_usd_lp_oracle.price() - - # Spot price - eth_from_lp = weeth_ng_pool.calc_withdraw_one_coin(10**18, 0) - usdt_from_lp = tricrypto_usdt_pool.get_dy(2, 0, eth_from_lp) - crvusd_from_lp = crvusd_usdt_pool.get_dy(0, 1, usdt_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.006 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.006 - - -def test_cvxcrv(stablecoin_aggregator): - cvxcrv_pool_address = "0x971add32Ea87f10bD192671630be3BE8A11b8623" - tricrv_pool_address = "0x4eBdF703948ddCEA3B11f675B4D1Fba9d2414A14" - - cvxcrv_pool = boa.from_etherscan(cvxcrv_pool_address, "cvxCRV/CRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - tricrv_pool = boa.from_etherscan(tricrv_pool_address, "TriCRV", uri=EXPLORER_URL, api_key=EXPLORER_TOKEN) - - crv_crvusd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRate.vy', [tricrv_pool_address], [0], [2]) # crvUSD/CRV - crv_usd_oracle = boa.load('contracts/price_oracles/CryptoFromPoolsRateWAgg.vy', [tricrv_pool_address], [0], [2], stablecoin_aggregator.address) # USD/CRV - cvxcrv_pool_crvusd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', cvxcrv_pool_address, crv_crvusd_oracle) # CRV/LP * crvUSD/CRV - cvxcrv_pool_usd_lp_oracle = boa.load('contracts/price_oracles/LPOracle.vy', cvxcrv_pool_address, crv_usd_oracle) # CRV/LP * crvUSD/CRV * USD/crvUSD - - # Oracle price - lp_oracle_price_crvusd = cvxcrv_pool_crvusd_lp_oracle.price() - lp_oracle_price_usd = cvxcrv_pool_usd_lp_oracle.price() - - # Spot price - crv_from_lp = cvxcrv_pool.calc_withdraw_one_coin(10**18, 0) - crvusd_from_lp = tricrv_pool.get_dy(2, 0, crv_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10**18 - - # assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.006 - # assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.006 - - trader = boa.env.generate_address() - erc_mock = boa.load_partial("contracts/testing/ERC20Mock.vy") - crv = erc_mock.at(cvxcrv_pool.coins(0)) - cvxcrv = erc_mock.at(cvxcrv_pool.coins(1)) - boa.deal(crv, trader, 25_000_000 * 10**18) - print("CRV pool balance:", crv.balanceOf(cvxcrv_pool) / 10**18, ", cvxCRV pool balance:", cvxcrv.balanceOf(cvxcrv_pool) / 10**18) - with boa.env.prank(trader): - - # --- DUMP --- - - print("\nTrade 5M CRV -> POOL -> X cvxCRV") - crv.approve(cvxcrv_pool, 2**256-1) - cvxcrv_pool.exchange(0, 1, 14_000_000 * 10**18, 0) - - # Align oracle - for i in range(100): - boa.env.time_travel(3600) - cvxcrv_pool.exchange(0, 1, 10**12, 0) - - # Oracle price - lp_oracle_price_crvusd = cvxcrv_pool_crvusd_lp_oracle.price_w() - lp_oracle_price_usd = cvxcrv_pool_usd_lp_oracle.price_w() - - # Spot price - crv_from_lp = cvxcrv_pool.calc_withdraw_one_coin(10 ** 18, 0) - crvusd_from_lp = tricrv_pool.get_dy(2, 0, crv_from_lp) - lp_spot_price_crvusd = crvusd_from_lp - lp_spot_price_usd = crvusd_from_lp * stablecoin_aggregator.price() // 10 ** 18 - - print("LP/crvUSD spot price:", lp_spot_price_crvusd / 10 ** 18, "LP/crvUSD oracle price:", lp_oracle_price_crvusd / 10 ** 18) - print("LP/USD spot price:", lp_spot_price_usd / 10 ** 18, "LP/USD oracle price:", lp_oracle_price_usd / 10 ** 18) - print("cvxCRV/CRV spot price:", cvxcrv_pool.get_dy(1, 0, 10**12) / 10 ** 12, "cvxCRV/CRV oracle price:", cvxcrv_pool.price_oracle() / 10 ** 18) - - assert abs(lp_spot_price_crvusd - lp_oracle_price_crvusd) / lp_oracle_price_crvusd < 0.0005 - assert abs(lp_spot_price_usd - lp_oracle_price_usd) / lp_oracle_price_usd < 0.0005 - - raise Exception("Success") diff --git a/tests_forked_ape/conftest.py b/tests_forked_ape/conftest.py deleted file mode 100644 index cd784bd6..00000000 --- a/tests_forked_ape/conftest.py +++ /dev/null @@ -1,331 +0,0 @@ -from pathlib import Path - -import pytest -from ape import accounts, Contract -from dotenv import load_dotenv -from .utils import deploy_test_blueprint, mint_tokens_for_testing - -BASE_DIR = Path(__file__).resolve().parent.parent -load_dotenv(Path(BASE_DIR, ".env")) - - -def pytest_configure(): - pytest.SHORT_NAME = "crvUSD" - pytest.FULL_NAME = "Curve.Fi USD Stablecoin" - pytest.rtokens = { - "USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7", - "USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "USDP": "0x8E870D67F660D95d5be530380D0eC0bd388289E1", - "TUSD": "0x0000000000085d4780B73119b644AE5ecd22b376", - } - pytest.ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" - pytest.POOL_NAME = "crvUSD/{name}" - pytest.POOL_SYMBOL = "crvUSD{name}" - pytest.STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID = 8 # reserve slot for crvusd plain pools factory - - pytest.stable_A = 500 # initially, can go higher later - pytest.stable_fee = 1000000 # 0.01% - pytest.stable_asset_type = 0 - pytest.stable_ma_exp_time = 866 # 10 min / ln(2) - - pytest.OWNERSHIP_ADMIN = "0x40907540d8a6C65c637785e8f8B742ae6b0b9968" - pytest.TRICRYPTO = "0xD51a44d3FaE010294C616388b506AcdA1bfAAE46" - - pytest.initial_pool_coin_balance = 500_000 # of both coins - pytest.initial_eth_balance = 1000 # both eth and weth - - # registry integration: - pytest.base_pool_registry = "0xDE3eAD9B2145bBA2EB74007e58ED07308716B725" - pytest.metaregistry = "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC" - pytest.address_provider = "0x0000000022D53366457F9d5E68Ec105046FC4383" - pytest.registry_name = "crvUSD plain pools" - pytest.new_id_created = False - pytest.max_id_before = 0 - pytest.handler_index = None - - # record stablecoin address: - pytest.stablecoin = pytest.ZERO_ADDRESS - - -""" -We use autouse=True to automatically deploy all during all tests -""" - - -@pytest.fixture(scope="module", autouse=True) -def forked_admin(accounts): - return accounts[0] - - -@pytest.fixture(scope="module", autouse=True) -def forked_fee_receiver(accounts): - return accounts[1] - - -@pytest.fixture(scope="module", autouse=True) -def weth(): - return Contract("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") - - -@pytest.fixture(scope="module", autouse=True) -def forked_user(project, accounts, weth): - acc = accounts[2] - mint_tokens_for_testing(project, acc, pytest.initial_pool_coin_balance, pytest.initial_eth_balance) - return acc - - -@pytest.fixture(scope="module", autouse=True) -def stablecoin(project, forked_admin): - _stablecoin = forked_admin.deploy(project.Stablecoin, pytest.FULL_NAME, pytest.SHORT_NAME) - pytest.stablecoin = _stablecoin.address - return _stablecoin - - -@pytest.fixture(scope="module", autouse=True) -def controller_impl(project, forked_admin): - return deploy_test_blueprint(project, project.Controller, forked_admin) - - -@pytest.fixture(scope="module", autouse=True) -def amm_impl(project, forked_admin): - return deploy_test_blueprint(project, project.AMM, forked_admin) - - -@pytest.fixture(scope="module") -def controller_factory( - project, - forked_admin, - stablecoin, - weth, - forked_fee_receiver, - controller_impl, - amm_impl, -): - factory = forked_admin.deploy( - project.ControllerFactory, - stablecoin.address, - forked_admin, - forked_fee_receiver, - weth.address, - ) - factory.set_implementations(controller_impl, amm_impl, sender=forked_admin) - stablecoin.set_minter(factory.address, sender=forked_admin) - return factory - - -@pytest.fixture(scope="module", autouse=True) -def stableswap_factory(project, forked_admin, forked_fee_receiver, stablecoin): - swap_factory = forked_admin.deploy(project.StableswapFactory, forked_fee_receiver) - swap_factory.add_token_to_whitelist(stablecoin, True, sender=forked_admin) - return swap_factory - - -@pytest.fixture(scope="module", autouse=True) -def owner_proxy(project, forked_admin, stableswap_factory): - # Ownership admin is account temporarily, will need to become OWNERSHIP_ADMIN - owner_proxy = forked_admin.deploy( - project.OwnerProxy, - forked_admin, - forked_admin, - forked_admin, - stableswap_factory, - pytest.ZERO_ADDRESS, - ) - - with accounts.use_sender(forked_admin): - stableswap_factory.commit_transfer_ownership(owner_proxy) - owner_proxy.accept_transfer_ownership(stableswap_factory) - return owner_proxy - - -@pytest.fixture(scope="module", autouse=True) -def stableswap_impl(project, forked_admin, stableswap_factory, owner_proxy): - # Set implementations - stableswap_impl = forked_admin.deploy(project.Stableswap) - - with accounts.use_sender(forked_admin): - owner_proxy.set_plain_implementations( - stableswap_factory, 2, [stableswap_impl.address] + [pytest.ZERO_ADDRESS] * 9 - ) - gauge_impl = Contract("0x5aE854b098727a9f1603A1E21c50D52DC834D846") - owner_proxy.set_gauge_implementation(stableswap_factory, gauge_impl) - return stableswap_impl - - -@pytest.fixture(scope="module", autouse=True) -def address_provider(stableswap_factory): - - address_provider = Contract("0x0000000022D53366457F9d5E68Ec105046FC4383") - - # Put factory in address provider / registry - with accounts.use_sender("0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a"): - address_provider_admin = Contract(address_provider.admin()) - pytest.max_id_before = address_provider.max_id() - - if address_provider.get_address(pytest.STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) == pytest.ZERO_ADDRESS: - - # this branch should not never be executed since the registry slot exists. - # we test this later. - address_provider_admin.execute( - address_provider, - address_provider.add_new_id.encode_input(stableswap_factory, pytest.registry_name), - ) - pytest.new_id_created = True - - else: - - address_provider_admin.execute( - address_provider, - address_provider.set_address.encode_input( - pytest.STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID, - stableswap_factory - ), - ) - - return address_provider - - -@pytest.fixture(scope="module", autouse=True) -def rtokens_pools(project, forked_admin, owner_proxy, stablecoin, stableswap_impl, stableswap_factory): - pools = {} - - # Deploy pools - for name, rtoken in pytest.rtokens.items(): - tx = owner_proxy.deploy_plain_pool( - pytest.POOL_NAME.format(name=name), - pytest.POOL_SYMBOL.format(name=name), - [rtoken, stablecoin.address, pytest.ZERO_ADDRESS, pytest.ZERO_ADDRESS], - pytest.stable_A, - pytest.stable_fee, - pytest.stable_asset_type, - 0, # implentation_idx - pytest.stable_ma_exp_time, - sender=forked_admin, - ) - # This is a workaround: instead of getting return_value we parse events to get the pool address - # This is because reading return_value in ape is broken - pool = project.Stableswap.at(tx.events.filter(stableswap_factory.PlainPoolDeployed)[0].pool) - pools[name] = pool - return pools - - -@pytest.fixture(scope="module", autouse=True) -def factory_handler(project, stableswap_factory, forked_admin): - return project.StableswapFactoryHandler.deploy( - stableswap_factory.address, pytest.base_pool_registry, sender=forked_admin - ) - - -@pytest.fixture(scope="module", autouse=True) -def metaregistry(address_provider, rtokens_pools, factory_handler): - - address_provider_admin = Contract(address_provider.admin()) - _metaregistry = Contract(pytest.metaregistry) - - previous_factory_handler = _metaregistry.find_pool_for_coins( - pytest.stablecoin, pytest.rtokens["USDP"], 0 - ) - factory_handler_integrated = previous_factory_handler != pytest.ZERO_ADDRESS - - with accounts.use_sender("0x7EeAC6CDdbd1D0B8aF061742D41877D7F707289a"): - - if not factory_handler_integrated: - - # first integration into metaregistry: - address_provider_admin.execute( - _metaregistry.address, - _metaregistry.add_registry_handler.encode_input(factory_handler), - ) - - else: # redeployment, which means update handler index in metaregistry. - - # get index of previous factory handler first: - for idx in range(1000): - if metaregistry.get_registry(idx) == previous_factory_handler: - break - - # update that idx with newly deployed factory handler: - address_provider_admin.execute( - _metaregistry.address, - _metaregistry.update_registry_handler.encode_input( - idx, factory_handler.address - ) - ) - - assert metaregistry.get_registry(idx) == factory_handler.address - - return _metaregistry - - -@pytest.fixture(scope="module", autouse=True) -def agg_stable_price(project, forked_admin, stablecoin, rtokens_pools): - agg = forked_admin.deploy(project.AggregateStablePrice, stablecoin, 10**15, forked_admin) - for pool in rtokens_pools.values(): - agg.add_price_pair(pool, sender=forked_admin) - agg.set_admin(pytest.OWNERSHIP_ADMIN, sender=forked_admin) # Alternatively, we can make it ZERO_ADDRESS - - return agg - - -@pytest.fixture(scope="module", autouse=True) -def peg_keepers(project, forked_admin, forked_fee_receiver, rtokens_pools, controller_factory, agg_stable_price): - peg_keepers = [] - for pool in rtokens_pools.values(): - peg_keeper = forked_admin.deploy( - project.PegKeeper, - pool, - 1, - forked_fee_receiver, - 2 * 10**4, - controller_factory, - agg_stable_price, - forked_admin.address, - ) - peg_keepers.append(peg_keeper) - - return peg_keepers - - -@pytest.fixture(scope="module", autouse=True) -def policy(project, forked_admin, peg_keepers, controller_factory, agg_stable_price): - return forked_admin.deploy( - project.AggMonetaryPolicy, - forked_admin, - agg_stable_price, - controller_factory, - peg_keepers + [pytest.ZERO_ADDRESS], - 627954226, # rate = 2% - 2 * 10**16, # sigma - 5 * 10**16, - ) # Target debt fraction - - -@pytest.fixture(scope="module", autouse=True) -def price_oracle(project, forked_admin, rtokens_pools, agg_stable_price): - return forked_admin.deploy( - project.CryptoWithStablePrice, - pytest.TRICRYPTO, - 1, # price index with ETH - rtokens_pools["USDT"], - agg_stable_price, - 600, - ) - - -@pytest.fixture(scope="module") -def chainlink_aggregator(): - return Contract("0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419") - - -@pytest.fixture(scope="module", autouse=True) -def price_oracle_with_chainlink(project, forked_admin, rtokens_pools, agg_stable_price, chainlink_aggregator): - return forked_admin.deploy( - project.CryptoWithStablePriceAndChainlink, - pytest.TRICRYPTO, - 1, # price index with ETH - rtokens_pools["USDT"], - agg_stable_price, - chainlink_aggregator.address, - 600, - 1 - ) diff --git a/tests_forked_ape/test_lendborrow.py b/tests_forked_ape/test_lendborrow.py deleted file mode 100644 index 080634d0..00000000 --- a/tests_forked_ape/test_lendborrow.py +++ /dev/null @@ -1,218 +0,0 @@ -import pytest -from ape import accounts as ape_accounts, Contract, exceptions -from .utils import mint_tokens_for_testing - - -class TestLendAndSwaps: - @pytest.fixture() - def factory_with_market(self, forked_admin, controller_factory, weth, price_oracle_with_chainlink, policy): - controller_factory.add_market( - weth, - 100, - 10**16, - 0, - price_oracle_with_chainlink, - policy, - 5 * 10**16, - 2 * 10**16, - 4 * pytest.initial_pool_coin_balance * 10**18, - sender=forked_admin, - ) - - @pytest.fixture() - def stablecoin_lend(self, project, forked_user, controller_factory, factory_with_market, weth, stablecoin): - with ape_accounts.use_sender(forked_user): - controller = project.Controller.at(controller_factory.controllers(0)) - weth.approve(controller.address, 2**256 - 1) - weth_amount = pytest.initial_eth_balance * 10**18 - controller.create_loan( - 2 * weth_amount, 4 * pytest.initial_pool_coin_balance * 10**18, 30, value=weth_amount - ) - - def test_create_loan_works(self, project, forked_user, controller_factory, weth, stablecoin, factory_with_market): - with ape_accounts.use_sender(forked_user): - controller = project.Controller.at(controller_factory.controllers(0)) - weth.approve(controller.address, 2**256 - 1) - weth_amount = pytest.initial_eth_balance * 10**18 - controller.create_loan( - 2 * weth_amount, 4 * pytest.initial_pool_coin_balance * 10**18, 30, value=weth_amount - ) - - # not enough collateral and not enough debt ceiling for controller - @pytest.mark.parametrize( - ("weth_multiplier", "coin_multiplier", "error_msg"), - ((0.01, 1, "Debt too high"), (1, 1.01, "Transaction failed.")), - ) - def test_create_loan_fails( - self, - project, - forked_user, - controller_factory, - weth, - stablecoin, - factory_with_market, - weth_multiplier: float, - coin_multiplier: float, - error_msg: str, - ): - with pytest.raises(exceptions.ContractLogicError) as e: - with ape_accounts.use_sender(forked_user): - controller = project.Controller.at(controller_factory.controllers(0)) - weth.approve(controller.address, 2**256 - 1) - weth_amount = int(weth_multiplier * pytest.initial_eth_balance * 10**18) - controller.create_loan( - 2 * weth_amount, - int(4 * coin_multiplier * pytest.initial_pool_coin_balance * 10**18), - 30, - value=weth_amount, - ) - - assert str(e) == error_msg - - def test_lend_balance(self, forked_user, stablecoin_lend, stablecoin): - assert stablecoin.balanceOf(forked_user) == 4 * pytest.initial_pool_coin_balance * 10 ** stablecoin.decimals() - - def test_controller( - self, project, forked_admin, forked_user, controller_factory, stablecoin_lend, stablecoin, weth - ): - controller = project.Controller.at(controller_factory.controllers(0)) - - with ape_accounts.use_sender(forked_user): - state = controller.user_state(forked_user) - assert state[0] == 2 * pytest.initial_eth_balance * 10**18 - assert state[1] == 0 - assert state[2] == controller.debt(forked_user) == 4 * pytest.initial_pool_coin_balance * 10**18 - assert state[3] == 30 - - # repay half - stablecoin.approve(controller.address, 2**256 - 1) - controller.repay(2 * pytest.initial_pool_coin_balance * 10**18) - assert stablecoin.balanceOf(forked_user) == 2 * pytest.initial_pool_coin_balance * 10**18 - - state = controller.user_state(forked_user) - assert state[0] == 2 * pytest.initial_eth_balance * 10**18 - assert state[1] == 0 - assert state[2] == controller.debt(forked_user) - assert ( - 2 * pytest.initial_pool_coin_balance * 10**18 - <= state[2] - <= 2 * pytest.initial_pool_coin_balance * 10**18 * 10001 // 10000 - ) - assert state[3] == 30 - - # withdraw eth - controller.remove_collateral(pytest.initial_eth_balance * 10**18, False) - assert weth.balanceOf(forked_user) == pytest.initial_eth_balance * 10**18 - - state = controller.user_state(forked_user) - assert state[0] == pytest.initial_eth_balance * 10**18 - assert state[1] == 0 - assert state[2] == controller.debt(forked_user) - assert ( - 2 * pytest.initial_pool_coin_balance * 10**18 - <= state[2] - <= 2 * pytest.initial_pool_coin_balance * 10**18 * 10001 // 10000 - ) - assert state[3] == 30 - - controller_factory.set_debt_ceiling( - controller.address, 20 * pytest.initial_pool_coin_balance * 10**18, sender=forked_admin - ) - - # borrow more without collateral - max_borrowable = controller.max_borrowable(pytest.initial_eth_balance * 10**18, 30) - borrow_amount = (max_borrowable - state[2]) // 2 # half of maximum - controller.borrow_more(0, borrow_amount) - assert stablecoin.balanceOf(forked_user) == 2 * pytest.initial_pool_coin_balance * 10**18 + borrow_amount - - # borrow more with collateral - resulting_balance = 2 * pytest.initial_pool_coin_balance * 10**18 + 3 * borrow_amount - controller.borrow_more(pytest.initial_eth_balance * 10**18, 2 * borrow_amount) - assert stablecoin.balanceOf(forked_user) == resulting_balance - - @property - def pool_coin_balance(self): - return pytest.initial_pool_coin_balance // 2 - - @property - def user_coin_balance(self): - return pytest.initial_pool_coin_balance - self.pool_coin_balance - - @pytest.fixture() - def rtokens_pools_with_liquidity( - self, - forked_user, - stablecoin, - stablecoin_lend, - rtokens_pools, - ): - with ape_accounts.use_sender(forked_user): - for rtoken_name, rtoken_address in pytest.rtokens.items(): - rtoken = Contract(rtoken_address) - pool = rtokens_pools[rtoken_name] - rtoken.approve(pool.address, 2**256 - 1) - stablecoin.approve(pool.address, 2**256 - 1) - - pool.add_liquidity( - [ - self.pool_coin_balance * 10 ** rtoken.decimals(), - self.pool_coin_balance * 10 ** stablecoin.decimals(), - ], - 0, - ) - - return rtokens_pools - - def test_stableswap_liquidity(self, forked_user, rtokens_pools_with_liquidity, stablecoin): - for pool in rtokens_pools_with_liquidity.values(): - n_coins = 2 - addresses = [] - for n in range(n_coins): - addr = pool.coins(n) - addresses.append(addr) - - coin = stablecoin if addr == stablecoin.address else Contract(addr) - assert pool.balances(n) == self.pool_coin_balance * 10 ** coin.decimals() - - assert stablecoin.address in addresses - - def test_stableswap_swap(self, forked_user, rtokens_pools_with_liquidity, stablecoin): - with ape_accounts.use_sender(forked_user): - for pool in rtokens_pools_with_liquidity.values(): - min_value = int(self.pool_coin_balance / 2 * 0.99) # for a half of liquidity - decimals_0 = Contract(pool.coins(0)).decimals() - decimals_1 = stablecoin.decimals() - - assert pool.get_dy(0, 1, self.pool_coin_balance // 2 * 10**decimals_0) >= min_value * 10**decimals_1 - assert pool.get_dy(1, 0, self.pool_coin_balance // 2 * 10**decimals_1) >= min_value * 10**decimals_0 - - pool.exchange(0, 1, self.pool_coin_balance // 2 * 10**decimals_0, min_value * 10**decimals_1) - assert stablecoin.balanceOf(forked_user) >= (min_value + self.user_coin_balance) * 10**decimals_1 - - def test_full_repay( - self, project, accounts, forked_admin, controller_factory, rtokens_pools_with_liquidity, stablecoin - ): - user = accounts[3] - lend_amount = 1000 - mint_tokens_for_testing(project, user, 100_000, lend_amount) - - controller = project.Controller.at(controller_factory.controllers(0)) - controller_factory.set_debt_ceiling( - controller.address, 8 * pytest.initial_pool_coin_balance * 10**18, sender=forked_admin - ) - - pool = rtokens_pools_with_liquidity["USDT"] - usdt = Contract("0xdAC17F958D2ee523a2206206994597C13D831ec7") - - with ape_accounts.use_sender(user): - controller.create_loan(lend_amount * 10**18, 1_000_000 * 10**18, 30, value=lend_amount * 10**18) - assert controller.loan_exists(user.address) - - # need a little bt more to full repay - usdt.approve(pool.address, 2**256 - 1) - pool.exchange(0, 1, 100_000 * 10 ** usdt.decimals(), 1000 * 10**18) - - # full repay - stablecoin.approve(controller.address, 2**256 - 1) - controller.repay(stablecoin.balanceOf(user)) - assert not controller.loan_exists(user.address) diff --git a/tests_forked_ape/test_registry_integration.py b/tests_forked_ape/test_registry_integration.py deleted file mode 100644 index 6fca575f..00000000 --- a/tests_forked_ape/test_registry_integration.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - - -def test_address_provider_entry(stableswap_factory, address_provider): - - assert address_provider.get_address(pytest.STABLESWAP_FACTORY_ADDRESS_PROVIDER_ID) == stableswap_factory.address - assert not pytest.new_id_created # this branch should never happen on mainnet since - assert address_provider.max_id() == pytest.max_id_before - - -def test_factory_handler_integration(metaregistry, stableswap_factory, factory_handler): - assert metaregistry.get_base_registry(factory_handler) == stableswap_factory - - -def test_rtoken_pools_in_metaregistry(metaregistry, rtokens_pools): - - for pool_address in rtokens_pools.values(): - assert metaregistry.is_registered(pool_address) - assert metaregistry.get_pool_from_lp_token(pool_address) == pool_address - - for token_address in pytest.rtokens.values(): - pool_addr = metaregistry.find_pool_for_coins(pytest.stablecoin, token_address, 0) - assert pool_addr != pytest.ZERO_ADDRESS - assert pool_addr in rtokens_pools.values() diff --git a/tests_forked_ape/utils.py b/tests_forked_ape/utils.py deleted file mode 100644 index 8e3968f3..00000000 --- a/tests_forked_ape/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -from ape import Contract, Project -from ape.contracts import ContractContainer - - -def deploy_test_blueprint(project: Project, contract: Contract, account): - initcode = contract.contract_type.deployment_bytecode.bytecode - - if isinstance(initcode, str): - initcode = bytes.fromhex(initcode.removeprefix("0x")) - - initcode = b"\xfe\x71\x00" + initcode # eip-5202 preamble version 0 - - initcode = b"\x61" + len(initcode).to_bytes(2, "big") + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + initcode - - tx = project.provider.network.ecosystem.create_transaction( - chain_id=project.provider.chain_id, - data=initcode, - gas_price=project.provider.gas_price, - nonce=account.nonce, - ) - - tx.gas_limit = project.provider.estimate_gas_cost(tx) - tx = account.sign_transaction(tx) - receipt = project.provider.send_transaction(tx) - return receipt.contract_address - - -def mint_tokens_for_testing(project: Project, account, stablecoin_amount: int, eth_amount: int): - """ - Provides given account with 1M of stablecoins - USDC, USDT, USDP and TUSD and with 1000 ETH and WETH - Can be used only on local forked mainnet - - :return: None - """ - - # USDC - token_contract = Contract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") - token_minter = "0xe982615d461dd5cd06575bbea87624fda4e3de17" - project.provider.set_balance(token_minter, 10**18) - amount = stablecoin_amount * 10 ** token_contract.decimals() - token_contract.configureMinter(token_minter, amount, sender=token_minter) - token_contract.mint(account, amount, sender=token_minter) - assert token_contract.balanceOf(account.address) >= amount - - # USDT - token_contract = Contract("0xdAC17F958D2ee523a2206206994597C13D831ec7") - token_owner = "0xc6cde7c39eb2f0f0095f41570af89efc2c1ea828" - project.provider.set_balance(token_owner, 10**18) - amount = stablecoin_amount * 10 ** token_contract.decimals() - token_contract.issue(amount, sender=token_owner) - token_contract.transfer(account, amount, sender=token_owner) - assert token_contract.balanceOf(account.address) >= amount - - # USDP - token_contract = Contract("0x8E870D67F660D95d5be530380D0eC0bd388289E1") - token_supply_controller = token_contract.supplyController() - project.provider.set_balance(token_supply_controller, 10**18) - amount = stablecoin_amount * 10 ** token_contract.decimals() - token_contract.increaseSupply(amount, sender=token_supply_controller) - token_contract.transfer(account, amount, sender=token_supply_controller) - assert token_contract.balanceOf(account.address) >= amount - - # TUSD - token_contract = Contract("0x0000000000085d4780B73119b644AE5ecd22b376") - # apply proxy - try: - token_impl = ContractContainer(Contract(token_contract.implementation()).contract_type) - token_contract = token_impl.at("0x0000000000085d4780B73119b644AE5ecd22b376") - except AttributeError: - # already applied - pass - - token_owner = token_contract.owner() - project.provider.set_balance(token_owner, 10**18) - amount = stablecoin_amount * 10 ** token_contract.decimals() - token_contract.mint(account, amount, sender=token_owner) - assert token_contract.balanceOf(account.address) >= amount - - # ETH - # Set balance to twice amount + 1 - half will be wrapped + (potential) gas - project.provider.set_balance(account.address, (2 * eth_amount + 1) * 10**18) - assert account.balance >= 2 * eth_amount * 10**18 - - # WETH - weth_contract = Contract("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") - weth_contract.deposit(value=eth_amount * 10**18, sender=account) - assert weth_contract.balanceOf(account.address) >= eth_amount * 10**18 diff --git a/tests_leverage/test_v1/utils.py b/tests_leverage/test_v1/utils.py deleted file mode 100644 index 32b60760..00000000 --- a/tests_leverage/test_v1/utils.py +++ /dev/null @@ -1,1524 +0,0 @@ -from ape import Contract, Project, accounts - - -def mint_tokens_for_testing(project: Project, account): - """ - Provides given account with 100 WBTC and 1000 ETH, sfrxETH, wstETH - Can be used only on local forked mainnet - - :return: None - """ - - # WBTC - token_contract = Contract("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599") - token_owner = "0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656" - project.provider.set_balance(token_owner, 10**20) - amount = 100 * 10**8 - token_contract.transfer(account, amount, sender=accounts[token_owner]) - assert token_contract.balanceOf(account.address) >= amount - - # tBTC - token_contract = Contract("0x18084fbA666a33d37592fA2633fD49a74DD93a88") - token_owner = "0x3ee18B2214AFF97000D974cf647E7C347E8fa585" # Wormhole: Portal Token Bridge - project.provider.set_balance(token_owner, 10**20) - amount = 100 * 10 ** 18 - token_contract.transfer(account, amount, sender=accounts[token_owner]) - assert token_contract.balanceOf(account.address) >= amount - - # ETH - # Set balance to twice amount + 1 - half will be wrapped + (potential) gas - project.provider.set_balance(account.address, 1000 * 10**18) - assert account.balance >= 1000 * 10**18 - - # sfrxETH - token_contract = Contract("0xac3e018457b222d93114458476f3e3416abbe38f") - token_owner = "0xBA12222222228d8Ba445958a75a0704d566BF2C8" - project.provider.set_balance(token_owner, 10**20) - amount = 1000 * 10**18 - token_contract.transfer(account, amount, sender=accounts[token_owner]) - assert token_contract.balanceOf(account.address) >= amount - - # wstETH - token_contract = Contract("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0") - token_owner = "0x0B925eD163218f6662a35e0f0371Ac234f9E9371" - project.provider.set_balance(token_owner, 10**20) - amount = 1000 * 10 ** 18 - token_contract.transfer(account, amount, sender=accounts[token_owner]) - assert token_contract.balanceOf(account.address) >= amount - - -def mint_crvusd_tokens_for_testing(project: Project, account): - """ - Provides given account with 100M crvUSD - Can be used only on local forked mainnet - - :return: None - """ - - # crvUSD - token_contract = Contract("0xf939e0a03fb07f59a73314e73794be0e57ac1b4e") - token_owner = "0xA920De414eA4Ab66b97dA1bFE9e6EcA7d4219635" # WETH controller - project.provider.set_balance(token_owner, 10**20) - amount = 100_000_000 * 10 ** 18 - token_contract.transfer(account, amount, sender=accounts[token_owner]) - assert token_contract.balanceOf(account.address) >= amount - - -CRVUSD = "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e" - -frxETH = "0x5E8422345238F34275888049021821E8E08CAa1f" -stETH = "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - -COLLATERALS = { - "sfrxETH": "0xac3E018457B222d93114458476f3E3416Abbe38F", - "wstETH": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "WBTC": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", - "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "sfrxETH2": "0xac3E018457B222d93114458476f3E3416Abbe38F", - "tBTC": "0x18084fba666a33d37592fa2633fd49a74dd93a88", -} - -CONTROLLERS = { - "sfrxETH": "0x8472A9A7632b173c8Cf3a86D3afec50c35548e76", - "wstETH": "0x100daa78fc509db39ef7d04de0c1abd299f4c6ce", - "WBTC": "0x4e59541306910ad6dc1dac0ac9dfb29bd9f15c67", - "WETH": "0xa920de414ea4ab66b97da1bfe9e6eca7d4219635", - "sfrxETH2": "0xec0820efafc41d8943ee8de495fc9ba8495b15cf", - "tBTC": "0x1c91da0223c763d2e0173243eadaa0a2ea47e704", -} - -LLAMMAS = { - "sfrxETH": "0x136e783846ef68c8bd00a3369f787df8d683a696", - "wstETH": "0x37417b2238aa52d0dd2d6252d989e728e8f706e4", - "WBTC": "0xe0438eb3703bf871e31ce639bd351109c88666ea", - "WETH": "0x1681195c176239ac5e72d9aebacf5b2492e0c4ee", - "sfrxETH2": "0xfa96ad0a9e64261db86950e2da362f5572c5c6fd", - "tBTC": "0xf9bd9da2427a50908c4c6d1599d8e62837c2bcb0", -} - -ROUTERS = { - "sfrxETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", - "wstETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", - "WBTC": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", - "WETH": "0x99a58482bd75cbab83b27ec03ca68ff489b5788f", - "sfrxETH2": "0xF0d4c12A5768D806021F80a262B4d39d26C58b8D", - "tBTC": "0xF0d4c12A5768D806021F80a262B4d39d26C58b8D", -} - -CRVUSD_POOLS = { - "USDC": "0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E", - "USDT": "0x390f3595bCa2Df7d23783dFd126427CCeb997BF4", - "USDP": "0xCa978A0528116DDA3cbA9ACD3e68bc6191CA53D0", - "TUSD": "0x34D655069F4cAc1547E4C8cA284FfFF5ad4A8db0", - "FRAX": "0x0cd6f267b2086bea681e922e19d40512511be538", -} - -ROUTER_PARAMS = { - "sfrxETH": { - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> frxeth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - frxETH, - ], - "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2 -> frxeth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - frxETH, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> frxeth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - frxETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> frxeth", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - frxETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2 -> frxeth", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - frxETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "wstETH": { - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> steth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - stETH, - ], - "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2 -> steth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - stETH, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 2, 3], [0, 1, 1], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> steth", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - stETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> steth", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - stETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2 -> steth", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - stETH, - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 1, 1]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "WBTC": { - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [1, 2, 1], [0, 1, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 1, 3], [0, 0, 0], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 1, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "WETH": { - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [1, 2, 1], [0, 2, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 2, 3], [0, 0, 0], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1], [0, 3, 2], [0, 2, 3], [0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "sfrxETH2": { - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', - COLLATERALS["sfrxETH"], - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 2, 1, 1, 3], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["USDC"], - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', - COLLATERALS["sfrxETH"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["USDT"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "crvUSD/USDP -> factory-v2-59 (USDP) -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDP"], - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', - COLLATERALS["sfrxETH"], - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["USDP"], - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', - COLLATERALS["sfrxETH"], - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["TUSD"], - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2 -> frxETH minter -> sfrxETH wrapper", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xbafa44efe7901e04e39dad13167d089c559c1138', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xac3e018457b222d93114458476f3e3416abbe38f', - COLLATERALS["sfrxETH"], - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 2, 1, 3, 3], [0, 0, 8, 0, 0], [0, 0, 8, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["FRAX"], - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "tBTC": { - "tbtc": { - "name": "factory-tricrypto-2 (TricryptoLLAMA)", - "route": [ - CRVUSD, - '0x2889302a794da87fbf1d6db415c1492194663d13', - COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 1, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x2889302a794da87fbf1d6db415c1492194663d13', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdc": { - "name": "crvUSD/USDC -> 3pool -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDC"], - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 2, 1, 1, 3], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["USDC"], - '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "crvUSD/USDT -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", - "route": [ - CRVUSD, - CRVUSD_POOLS["USDT"], - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["USDT"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "crvUSD/TUSD -> tusd -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", - "route": [ - CRVUSD, - CRVUSD_POOLS["TUSD"], - '0x0000000000085d4780b73119b644ae5ecd22b376', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["TUSD"], - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "crvUSD/FRAX -> frax -> tricrypto2 -> factory-crvusd-16 (tBTC/WBTC)", - "route": [ - CRVUSD, - CRVUSD_POOLS["FRAX"], - '0x853d955acef822db058eb8505911ed77f175b99e', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - COLLATERALS["WBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - COLLATERALS["tBTC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [0, 3, 2, 1, 4], [0, 1, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - CRVUSD_POOLS["FRAX"], - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x0000000000000000000000000000000000000000', - ], - }, - }, -} - -ROUTER_PARAMS_DELEVERAGE = { - "sfrxETH": { - "usdc": { - "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - ], - }, - "tricrv": { - "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - ], - }, - "frax": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - ], - }, - }, - "wstETH": { - "usdc": { - "name": "wstETH wrapper -> steth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "wstETH wrapper -> steth -> tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - ], - }, - "tricrv": { - "name": "wstETH wrapper -> steth -> factory-tricrypto-4 (TriCRV)", - "route": [ - COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "wstETH wrapper -> steth -> tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - ], - }, - "frax": { - "name": "wstETH wrapper -> steth -> tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["wstETH"], - '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', - '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xdc24316b9ae028f1497c275eb9192a3ea0f67022', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - ], - }, - }, - "WBTC": { - "usdc": { - "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["WBTC"], - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdp": { - "name": "tricrypto2 -> factory-v2-59 (USDP) -> crvUSD/USDP", - "route": [ - COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - '0x8e870d67f660d95d5be530380d0ec0bd388289e1', - CRVUSD_POOLS["USDP"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xc270b3b858c335b6ba5d5b10e2da8a09976005ad', - CRVUSD_POOLS["USDP"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["WBTC"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "WETH": { - "usdc": { - "name": "factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["WETH"], - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tricrv": { - "name": "factory-tricrypto-4 (TriCRV)", - "route": [ - COLLATERALS["WETH"], - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["WETH"], - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - }, - "sfrxETH2": { - "usdc": { - "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - ], - }, - "tricrv": { - "name": "sfrxETH wrapper -> frxeth -> factory-tricrypto-4 (TriCRV)", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - ], - }, - "frax": { - "name": "sfrxETH wrapper -> frxeth -> tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["sfrxETH"], - '0xac3e018457b222d93114458476f3e3416abbe38f', - '0x5e8422345238f34275888049021821e8e08caa1f', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - ], - "swap_params": [[0, 0, 8, 0, 0], [1, 0, 1, 1, 2], [2, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2]], - "factory_swap_addresses": [ - '0x0000000000000000000000000000000000000000', - '0xa1f8a6807c402e4a15ef4eba36528a3fed24e577', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - ], - }, - }, - "tBTC": { - "tbtc": { - "name": "factory-tricrypto-2 (TricryptoLLAMA)", - "route": [ - COLLATERALS["tBTC"], - '0x2889302a794da87fbf1d6db415c1492194663d13', - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 3, 3], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0x2889302a794da87fbf1d6db415c1492194663d13', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdc": { - "name": "factory-crvusd-16 (tBTC/WBTC) -> factory-tricrypto-0 (TricryptoUSDC) -> crvUSD/USDC", - "route": [ - COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - CRVUSD_POOLS["USDC"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x7f86bf177dd4f3494b841a37e810a34dd56c829b', - CRVUSD_POOLS["USDC"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "usdt": { - "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> crvUSD/USDT", - "route": [ - COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - CRVUSD_POOLS["USDT"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - CRVUSD_POOLS["USDT"], - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - }, - "tusd": { - "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> tusd -> crvUSD/TUSD", - "route": [ - COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - '0x0000000000085d4780b73119b644ae5ecd22b376', - CRVUSD_POOLS["TUSD"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xecd5e75afb02efa118af914515d6521aabd189f1', - CRVUSD_POOLS["TUSD"], - '0x0000000000000000000000000000000000000000', - ], - }, - "frax": { - "name": "factory-crvusd-16 (tBTC/WBTC) -> tricrypto2 -> frax -> crvUSD/FRAX", - "route": [ - COLLATERALS["tBTC"], - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xdac17f958d2ee523a2206206994597c13d831ec7', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - '0x853d955acef822db058eb8505911ed77f175b99e', - CRVUSD_POOLS["FRAX"], - CRVUSD, - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - ], - "swap_params": [[1, 0, 1, 1, 2], [1, 0, 1, 3, 3], [3, 0, 2, 1, 4], [0, 1, 1, 1, 2], [0, 0, 0, 0, 0]], - "factory_swap_addresses": [ - '0xb7ecb2aa52aa64a717180e030241bc75cd946726', - '0xd51a44d3fae010294c616388b506acda1bfaae46', - '0xd632f22692fac7611d2aa1c0d552930d43caed3b', - CRVUSD_POOLS["FRAX"], - '0x0000000000000000000000000000000000000000', - ], - }, - } -} diff --git a/tests_leverage/test_v2/tests/test_leverage.py b/tests_leverage/test_v2/tests/test_leverage.py deleted file mode 100644 index 4d0d160d..00000000 --- a/tests_leverage/test_v2/tests/test_leverage.py +++ /dev/null @@ -1,16 +0,0 @@ -class TestLeverage: - class TestCreateLoan: - def test_max_borrowable(self, alice, collateral, controller, leverage_zap_1inch, router_api_1inch): - N = 4 - max_collateral = 0 - collateral_decimals = 10 ** collateral.decimals() - p_avg = 10**18 * collateral_decimals // router_api_1inch.get_rate(collateral.address, 1 * 10**18) - balance = 1 * 10**18 if collateral.symbol() == "WETH" else collateral.balanceOf(alice) - - max_borrowable = leverage_zap_1inch.max_borrowable(controller.address, balance, max_collateral, N, p_avg) - - max_collateral = router_api_1inch.get_rate(collateral.address, max_borrowable) - p_avg = max_borrowable * collateral_decimals // max_collateral - max_borrowable = leverage_zap_1inch.max_borrowable(controller.address, balance, max_collateral, N, p_avg) - - assert max_borrowable > 0 diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..3a799b91 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2111 @@ +version = 1 +requires-python = ">=3.10, <4" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "bitarray" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c1/644ea86b6f1a0864f656a3b3ee5bf8c29daa895cb3233942315fe065ea3a/bitarray-3.7.2.tar.gz", hash = "sha256:27a59bb7c64c0d094057a3536e15fdd693f8520771ee75d9344b82d0a5ade2d0", size = 150586 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/78/488d06983f34f974c857ff6494dc8ef4f4b9d5e237d91d99ba47298bf446/bitarray-3.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10b53d85e98c5e6189f2c5e9744171901eec4fc7b7d5a0ea47d0902fb9738abc", size = 147139 }, + { url = "https://files.pythonhosted.org/packages/d5/aa/8ebe4ed9c2acba77d49e4a2c1fd25fefb841ceaba39bf6b3949c6c6e1a18/bitarray-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7770f1b8f880acca6743da21665578082378014415c85454ca9eabf41c06b1d1", size = 144090 }, + { url = "https://files.pythonhosted.org/packages/2f/20/0d93b1b54cb8944e582109814712fe37d934c8cd51d3faeaa69effacac0a/bitarray-3.7.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83f074d9c52565cd14ec24f9895d6bbab14f123ac64c81415c2911d09b827ea", size = 321801 }, + { url = "https://files.pythonhosted.org/packages/88/e8/bca798cdd87d9bd54f77bb0f304cd61c9a83d7610cc0c7c287207a6c3c39/bitarray-3.7.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5abc46f51e5414ef792172ee52e6af1f43d15cfab32f326b26e52be860476a25", size = 349665 }, + { url = "https://files.pythonhosted.org/packages/f3/d3/052aca85c4d92f2ee221598249bd8332d72c716255beedbbb8799660909f/bitarray-3.7.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7f950ac47ead99959fca0017de8af3e966c16654399d04d6da84e395d7f1e3d", size = 359949 }, + { url = "https://files.pythonhosted.org/packages/7b/6e/0633ffbbe621fc1820765b509deff640a34a35ccb8400ca2f574a50daf92/bitarray-3.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bfaf2f91616fca3025804100effa9153da8bfb3312232cfea9e2917956fd15f", size = 328142 }, + { url = "https://files.pythonhosted.org/packages/30/68/9d34bbef00de3caf2d5a555c985b265736d264f884f54bb74d2735795f0d/bitarray-3.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f7cadb42a1a71203dfa3717e22a26779b60e7eea224efc063e11615ceeffe02d", size = 319634 }, + { url = "https://files.pythonhosted.org/packages/29/88/f27c5f68fb4b59d873e63a036b71735900602caf62029153ed5fa72cad7f/bitarray-3.7.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:041fb7c29156b0d2779d600abdcf632e67e2e2b93664a00cd39d7d492a769ac4", size = 347811 }, + { url = "https://files.pythonhosted.org/packages/92/a9/a3a60a4ae40c1ca726ea856e3e66090f9290d36df95fe179f01c8e3e3d00/bitarray-3.7.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3ffb9a746282687b26d3475ab2288fa932e8321acdeae8b5cef26c0daeb43e4c", size = 344003 }, + { url = "https://files.pythonhosted.org/packages/59/34/d9a7dc4187a72c30c7d98c995ba1b77f3280c5afe64e7409a3257c66d938/bitarray-3.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042076dd5663401a6c881e8da40b883409cb1c7dbfb11959d80c0961d62ae388", size = 325038 }, + { url = "https://files.pythonhosted.org/packages/a4/c8/926a2833ab80e15b5514d8270fb4b69ae858a84fadfe64c968ca663e35de/bitarray-3.7.2-cp310-cp310-win32.whl", hash = "sha256:90265fa81144e62ec00038796096d53a0b4e6b695c26628dc19a6a1805d018d8", size = 141257 }, + { url = "https://files.pythonhosted.org/packages/be/88/f80eab0d7501bcfdf59efc2dd5e4da4784af7d1ccec263c3fda14a9d8bac/bitarray-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:72d7c86fd64dbad5b8e35eddff90f63a2db380760b84dab1287b0fe5a112eb76", size = 148071 }, + { url = "https://files.pythonhosted.org/packages/f0/43/0ebf3c1f8aa39135f43c45f64c2b86bcc1bf8d0a1cc7cba43fe5e11b09fa/bitarray-3.7.2-cp310-cp310-win_arm64.whl", hash = "sha256:0bf145b35ce851778aa14c922376f0801a66b5d9cc69ed37b94a9582ff3c6af8", size = 145059 }, + { url = "https://files.pythonhosted.org/packages/39/92/267e7234faefe3cddd3b1360b4c2946a2ebf7179513b8c7a5744f8b2ffd0/bitarray-3.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f0b36a06fd7416feb2bf4651cf9ebd692ab457df38f2cf577209ba4b805bb4b6", size = 147140 }, + { url = "https://files.pythonhosted.org/packages/4a/96/fb1081a580cc2ef7fdaba7bb8a6e34dafdef3b0c1697dbc6617216795987/bitarray-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee7c41ac961c5625791f463280c9de2dfb8cb1852843689c9701244a528b9b48", size = 144089 }, + { url = "https://files.pythonhosted.org/packages/fb/f6/beeb02dec52bc970db9789b75eebee07467e9b824aad66eba53a763eef15/bitarray-3.7.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fffd2387aee2df25da51ae232e038febc6537a34e25d986aa8d4a1b43547d994", size = 329875 }, + { url = "https://files.pythonhosted.org/packages/c9/a4/9d9fed4db42b4ac9e367786f007bde6cf41dcb3f0863b5176d95d8db5d10/bitarray-3.7.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:739153b628389e72e7a10a766c5f417a1f60e14f7d6c18e1d601c77ee3b03360", size = 357727 }, + { url = "https://files.pythonhosted.org/packages/0d/35/1371a5ce63d3238abfad179dcb371dbe2491c9a2f5fe7fac41c1216a5955/bitarray-3.7.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f69aedc2f995e8f046b6bf9625866fdff9d6502cc02df59f57ada4196a03647", size = 368179 }, + { url = "https://files.pythonhosted.org/packages/42/e3/85e699578e1eca1ad6104f6934596c10efc0cde0d87c75be331d8c7203a8/bitarray-3.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad2462fd80e12fc61776fbf4c0ec5b37ddc03cdbca479110329aa11ca5487e8", size = 335984 }, + { url = "https://files.pythonhosted.org/packages/c3/c6/785aa7925645c250cfa36e782cc078a0bc136140dd72293a51acab99a7bb/bitarray-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0480c8cab13646a362dd4b925f486af0186c4334cc9ddfd4083b56fdb39fc1d", size = 327573 }, + { url = "https://files.pythonhosted.org/packages/ed/2b/a06e84b85884666e2b784bd4d9f605484f5fedc6dd557729585a09ca5b28/bitarray-3.7.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0617cd06aef4d38c5fa81475efcfd0ca5b73683331736042381b8c58be2bd913", size = 355752 }, + { url = "https://files.pythonhosted.org/packages/66/06/57728ce036c1a8e740d22d229d325a7307d5aed07587a58a9aded7752b71/bitarray-3.7.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:13033bd04178695a93cbde503dde4a4f07f3b0d33927713ce55befffea5c20dc", size = 352164 }, + { url = "https://files.pythonhosted.org/packages/4b/fe/719e62f924073be1d935266964a6a186a67389b1ad8e876271ad5f2ac5b4/bitarray-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a52a00c73afbea40522346b11850338bbc3c83c54572f3fd5eb4d28e7bb5fb4", size = 333041 }, + { url = "https://files.pythonhosted.org/packages/73/f7/584d7f0c4267f845fb01d20fb16f9ad1d977d270557bfdd6b31824e9d08b/bitarray-3.7.2-cp311-cp311-win32.whl", hash = "sha256:4f9ea4014e7635570b609445ff10043eb23b60948f4fd70fb206fdc2b4cbcc60", size = 141405 }, + { url = "https://files.pythonhosted.org/packages/ac/b2/ec72d1233fb2dfbe6d248ba567818c26038644c270926b865da411d958c7/bitarray-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:682d23e555dfb729039ddc7f36ac6fce1c84811c441d91714b0cb75d14a6dc98", size = 148254 }, + { url = "https://files.pythonhosted.org/packages/2e/f1/6b6c76f92ba21a7eef847b8b5977c2b4f89736295616b996dc1da2ede9ca/bitarray-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:824cb8f94268249755dc0fb2d72e09ddc5a48a1d4915f1c709a37099ae5b1e64", size = 145352 }, + { url = "https://files.pythonhosted.org/packages/2d/6f/f91eda05b138e69e842c913461765b3cab4e22269f0ad756e530ae4aa932/bitarray-3.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0be3705631c15394231b205f19bfac1cfd67d86024c3ee0325305b8557303a8c", size = 147237 }, + { url = "https://files.pythonhosted.org/packages/21/90/dd90023aa54d698d1afdbcac2cc76f0b67840dc2c44334543c057b43817b/bitarray-3.7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48e2551ba3562464ed3b0a6d10ae3505cbcd63b5a5fb8effcf13c65d5a39931c", size = 144020 }, + { url = "https://files.pythonhosted.org/packages/49/15/7d5dc84ef3e8e12ec376ff06f1593c2f2cc5e16c9f3a1cb946b999031e78/bitarray-3.7.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f88ef6412eefee6bd99ad8b6f985f140da37e8e21cbcb84a4090be433267c8c9", size = 331886 }, + { url = "https://files.pythonhosted.org/packages/e3/0f/77a1de93cf3a5878f555bb5f689b3f4c97b41cc1f4a8fd4a02e9fee5b9aa/bitarray-3.7.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e113bc700a1c97fbb9442f129de9bcf10008bfafb5b12dc97f689d37002badd", size = 359759 }, + { url = "https://files.pythonhosted.org/packages/d7/b4/fe070c3903e9b7b03b8198110b1b5c2f80bf91bb8abfe926b7b5fae5b1b4/bitarray-3.7.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:de5c6f960f279f716571ffb9146a601d5f64921264c41f2fc4316b86f996a648", size = 370990 }, + { url = "https://files.pythonhosted.org/packages/ce/1a/fc71d713832d36b6221eee7f98a3422aa6febea1f55f3ee82fbbd5133d77/bitarray-3.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06ebdf0d663e38a6c77aeaec16b89c8bb00110696aae12ef369413990ed467da", size = 339304 }, + { url = "https://files.pythonhosted.org/packages/be/40/737018176f57265ec73164c98b7919345798eb984bd1ac311eb9eb156101/bitarray-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:890dd8431b6cc2c4cdaa24539d191c949802a11a20dca4cc0678452b1e527daf", size = 329607 }, + { url = "https://files.pythonhosted.org/packages/a8/40/47087cdde8a70c1a77754a4c6f8a7a636289a83fb14e3e0608bc010a4719/bitarray-3.7.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:43330e929846790ac6d76b52de58da5b550fcef0627b4632de01f405c223b612", size = 357175 }, + { url = "https://files.pythonhosted.org/packages/7d/0c/f06abce6637156efcfc836e4637e24be475478e5e81c9b050a1d1885e9c3/bitarray-3.7.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b9f8d8d116925d12ab9d2992b12781bf34eeb2a6a329dcf1ea1c7407e6c07e07", size = 355268 }, + { url = "https://files.pythonhosted.org/packages/03/3e/d50498496f97d12e65d48bb96e831db537b17344dd071293353171ed1633/bitarray-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddf01e86d866e6d321f71ee6e63d6381957797530125fa558ebca76b54567958", size = 335971 }, + { url = "https://files.pythonhosted.org/packages/a8/28/ad7a934b37a8d20cd7673d0dcb3b1c125a077059309abb555518a7901d64/bitarray-3.7.2-cp312-cp312-win32.whl", hash = "sha256:73a29c49a81426a1b0d153064045f3f4fde6cb88ae38ada1d99d200486cf53a3", size = 141539 }, + { url = "https://files.pythonhosted.org/packages/9f/20/6bbbd4309801ccad39624f66fc6407a3c9c95827074e8270591c9a6d3599/bitarray-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:283e5a5b735a7574a5242ed2ecbb0b09c9521ed78ff4067089efd2ba856e2332", size = 148533 }, + { url = "https://files.pythonhosted.org/packages/88/e8/eb9bb20c8ad309c0e404b4d7b9d0e37b0d265b842998fcc4e9a12cd6895e/bitarray-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:9c3f1d983c12dd1e54a808b78d685ccd9b96b7c43ef20fbf9b85fa076e491cec", size = 145496 }, + { url = "https://files.pythonhosted.org/packages/7f/2e/45239f89c02dde9059360d20ef8b1f3979da4547fafc14571b6a1f4560a1/bitarray-3.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0eacd088bbad701d691da4a90e19f39469665d323a3809b82cb9e5abaf30aeea", size = 147218 }, + { url = "https://files.pythonhosted.org/packages/c0/56/5f91439e970ed1ca7149e5a54bfa466b9142521378d7d972eab601ea5640/bitarray-3.7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dde42566197f8148daeed354c0dbb0450b834c4fda6a94645810de64d39328fc", size = 143999 }, + { url = "https://files.pythonhosted.org/packages/3e/2d/bbce096e1357615374707238e3e331d903771bdd2768fa7c955f1c21ef59/bitarray-3.7.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d595b7159318249064b94879b8a8d947e5ab11647ae975ade7e86b132bed091", size = 331956 }, + { url = "https://files.pythonhosted.org/packages/89/7e/34739b627b804087aa20748df7ac2ec64b01499817f603cda5eb80d81961/bitarray-3.7.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba9a45ff8a96ada0d215e5111971f1b432064e9ab0e1fae668603cb0023086eb", size = 359825 }, + { url = "https://files.pythonhosted.org/packages/cb/c5/d548f3ca9b9f413768c91b58d127240b0464d6964b98ed091cf5a3284de3/bitarray-3.7.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabfd2ebd43f295a4eb945a4e3ca7f4de63ce196341b7f25dcf464147d8fd5b3", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/95/a3/8acb092a2ae90539b4f2dac41f6aed36761c382d9f44ba8d2baab75bff6d/bitarray-3.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c10c893ce03693bf5084470c782429f242dc84e836a6442155f25c3ba77948de", size = 339372 }, + { url = "https://files.pythonhosted.org/packages/2d/a9/d265a635cf29ccfe0f7dcfd980b487c6ba82de3b9c13f2da07b25624eee8/bitarray-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:164ae38aed41f8ee663d2b4f950dc2502799a17cd2e5d004180c63b8f3640c72", size = 329601 }, + { url = "https://files.pythonhosted.org/packages/cc/91/f7f97b7094702972350af0e0d9305e677e93bdde0e772497c67038bd137f/bitarray-3.7.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3496f761d08ccda94a07cd782fc97b23c818dfc1aaef5551349004174aa0cb85", size = 357191 }, + { url = "https://files.pythonhosted.org/packages/96/7a/4530b77264e7ea887ba61fcb209a001871730720b1c6f47edc94a9190ac6/bitarray-3.7.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f18ca6039ec011e81a641cc622a168e7c4cbcf336bf854b7c075d49dd8dd85e0", size = 355262 }, + { url = "https://files.pythonhosted.org/packages/6c/da/d7f8e7078b9dd291cfb97ab5f45dde399b86b411e6c0345c63727fac48d2/bitarray-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c0e96c88f7bd202bde53ad0d58d0d1b669ab2745152ed4b909c5d7e80558b44b", size = 335986 }, + { url = "https://files.pythonhosted.org/packages/0e/8a/26f8dd9d14baa436b1a67b7460e684c16e26b92d2054675a99f982b445db/bitarray-3.7.2-cp313-cp313-win32.whl", hash = "sha256:5056531cbf9732cddacaf96b2732097c546f28a0a1b778e1d389852d43af7853", size = 141522 }, + { url = "https://files.pythonhosted.org/packages/f0/b9/c5cc21204d1457c42bcbbf93246e707f66fcd9ec93c2c57cb5f246386187/bitarray-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:ddc67e003e0065feaf70e529366425d0d5747a6487bbfffbec6f9e229960cdd6", size = 148540 }, + { url = "https://files.pythonhosted.org/packages/f3/5e/4ee20ac951069e30b87964239666ee5e572bacb9f60c515445b079465e4d/bitarray-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:ce782a6ee535042ea1bed8c57b5dbb45e59f208297abb079fa56a61aa8b120a6", size = 145505 }, + { url = "https://files.pythonhosted.org/packages/2a/d6/235e9cc42d0e254b2e7a9c52dcff4e7a3f6cb0d045c8f533f48c78d3121c/bitarray-3.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:019bbd454feff2607c2af171eb5e8268925aa24ce3d1b43bfd87f2f0dddefc0e", size = 147209 }, + { url = "https://files.pythonhosted.org/packages/82/1c/66179ed5f7b78583e8e4678bb68f6637cfcad5ea4febf46c3e4bada36e06/bitarray-3.7.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5f323773d6e3c22f25c2b9a2b96caee9a7aa5420861144f190ae0e183621e1b2", size = 144060 }, + { url = "https://files.pythonhosted.org/packages/e4/65/e3a977864a9c0150885cf583e066a0303a612b6e829cfe3c1170a1e672c9/bitarray-3.7.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95e5861b96b83b13d962f20b2e8fba26296e5cefde2c9015385e945798916da", size = 331856 }, + { url = "https://files.pythonhosted.org/packages/91/31/965f75c78378fadd22824910f5a19c90e9c4aebc3bc78cd576761cb0f4e4/bitarray-3.7.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ea5b4c553176b22438d89b4ec953124119dc0c5f51f80039947d5a49e920a3a7", size = 359879 }, + { url = "https://files.pythonhosted.org/packages/18/24/fb4e32b5345067971262310ca19d751b0e87c9e03d622939015e755b9967/bitarray-3.7.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:884792b4e6c19dc6529ca28f2de82133d31c52039eb0c4bc034ae4f8d19afee2", size = 370605 }, + { url = "https://files.pythonhosted.org/packages/54/33/1f861aa36b58c6d9351b71f9c26facb5badf0450d35b934cbe68df39bdfe/bitarray-3.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bff701d1852aed8c21ad071a284ff3ff51e1b48c36be39ea273a374cb7c971d", size = 339088 }, + { url = "https://files.pythonhosted.org/packages/f8/d7/6c891c2ef20ffbaa3a61272b1375849b7ba449fb236bd954588af80a45b9/bitarray-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eba43046de6ddaa2e917c189a25ae0a92c57ec9789c1a0ebd5cc9de1fab0d4f0", size = 329798 }, + { url = "https://files.pythonhosted.org/packages/d3/be/e956c75c07a8a06ccfbe0610dc2276ea656d0f2dabfd47adae1b0688d901/bitarray-3.7.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:de77dfd695e599ea2dabd0c3d990548cde8ace15eeeb55b17bddbb8d2eab67a0", size = 357447 }, + { url = "https://files.pythonhosted.org/packages/a1/16/4feb2544d21ba828d4d7f2e827060d8f278a30fba27c57d5e1561d3cf968/bitarray-3.7.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a6dea053e7e5bcabae669e6d7730b894283ef7611d035798d85df12522dae6ff", size = 354724 }, + { url = "https://files.pythonhosted.org/packages/b6/29/a49e9673d29646d659538b59c012df0e9d9201f84b5c84093d3810cef57b/bitarray-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:13985244301c1186760fa2e0107e838807c368fb1fc589601c54b72af0cf997c", size = 335984 }, + { url = "https://files.pythonhosted.org/packages/71/1e/cab11929caaed8290b5a5c280beccd00c492e1affbd7c4312de1dfc34810/bitarray-3.7.2-cp314-cp314-win32.whl", hash = "sha256:c8462c9dd4be7c68eacc407f5214056f310b989aa62ba26280ef992170e78ff3", size = 140698 }, + { url = "https://files.pythonhosted.org/packages/82/96/1d788e9e21c6600a0a13d6952edd2c5c2cb50a147536d72f9ea29ee986ea/bitarray-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5edb42097a39ae253e19b5c8343c0bda0b8a0df486b6fce548992fa9141a2af7", size = 147312 }, + { url = "https://files.pythonhosted.org/packages/08/ef/4dd74fd4a982b75bade2ce061dde8cbc52f7cadfffecca102edbc8f5dd8f/bitarray-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:6cab44b1963e54017fcda240a9a96d01f64fd9e03e29aea6e12cd49c0e0a1bc7", size = 144704 }, +] + +[[package]] +name = "cached-property" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", size = 10574 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428 }, +] + +[[package]] +name = "cbor2" +version = "5.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/b8/c0f6a7d46f816cb18b1fda61a2fe648abe16039f1ff93ea720a6e9fb3cee/cbor2-5.7.1.tar.gz", hash = "sha256:7a405a1d7c8230ee9acf240aad48ae947ef584e8af05f169f3c1bde8f01f8b71", size = 102467 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/08/a9b3e777ace829d9d782f0a80877085af24708d73bd1c41c296aeba4ebac/cbor2-5.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a0fc6cc50e0aa04e54792e7824e65bf66c691ae2948d7c012153df2bab1ee314", size = 67914 }, + { url = "https://files.pythonhosted.org/packages/5d/b5/1c23af80b279d5ec336c57e41a53bf8158e2ec3610b415cbc74887145d5d/cbor2-5.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c2fe69c1473d18d102f1e20982edab5bfa543fa1cda9888bdecc49f8b2f3d720", size = 68445 }, + { url = "https://files.pythonhosted.org/packages/f6/76/4d14dce9acd92333a249c676579e4879c492efda142c91c242044a70816d/cbor2-5.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34cbbe4fcf82080412a641984a0be43dfe66eac50a8f45596da63fde36189450", size = 254506 }, + { url = "https://files.pythonhosted.org/packages/b0/70/d835e91d53bc9df4d77764262489b6de505cfa400799a6625e9368391ea7/cbor2-5.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fc3d3f00aed397a1e4634b8e1780f347aad191a2e1e7768a233baadd4f87561", size = 247760 }, + { url = "https://files.pythonhosted.org/packages/29/c7/7fe1c82b5ddb00a407f016ca0de0560e47b3f6c15228478911b3b9ffb0e2/cbor2-5.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99e1666887a868e619096e9b5953734efd034f577e078f4efc5abd23dc1bcd32", size = 250188 }, + { url = "https://files.pythonhosted.org/packages/49/fd/40887b1aee3270284d2e9ac6740566a554cb6fd7ca9f251d7e1ee86ba1c3/cbor2-5.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59b78c90a5e682e7d004586fb662be6e451ec06f32fc3a738bbfb9576c72ecc9", size = 244190 }, + { url = "https://files.pythonhosted.org/packages/81/ba/9a91f4046c9a101fc68c23913c916d1fbcb6fae11d6a6f574f91c26ed31a/cbor2-5.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:6300e0322e52f831892054f1ccf25e67fa8040664963d358db090f29d8976ae4", size = 68150 }, + { url = "https://files.pythonhosted.org/packages/12/1e/aad24a2fe0b54353e19aaad06f7d7eb2d835dc4f5bbf5882f98be20e8744/cbor2-5.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:7badbde0d89eb7c8b9f7ef8e4f2395c02cfb24b514815656fef8e23276a7cd36", size = 64123 }, + { url = "https://files.pythonhosted.org/packages/52/67/319baac9c51de0053f58fa74a9548f93f3629aa3adeebd7d2c99d1379370/cbor2-5.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b1efbe6e82721be44b9faf47d0fd97b0150213eb6a4ba554f4947442bc4e13f", size = 67894 }, + { url = "https://files.pythonhosted.org/packages/2c/53/d23d0a234a4a098b019ac1cadd33631c973142fc947a68c4a38ca47aa5dc/cbor2-5.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb94bab27e00283bdd8f160e125e17dbabec4c9e6ffc8da91c36547ec1eb707f", size = 68444 }, + { url = "https://files.pythonhosted.org/packages/3a/a2/a6fa59e1c23b0bc77628d64153eb9fc69ac8dde5f8ed41a7d5316fcd0bcd/cbor2-5.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29f22266b5e08e0e4152e87ba185e04d3a84a4fd545b99ae3ebe42c658c66a53", size = 261600 }, + { url = "https://files.pythonhosted.org/packages/3d/cb/e0fa066aa7a09b15b8f56bafef6b2be19d9db31310310b0a5601af5c0128/cbor2-5.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25d4c7554d6627da781c9bd1d0dd0709456eecb71f605829f98961bb98487dda", size = 254904 }, + { url = "https://files.pythonhosted.org/packages/2c/d5/b1fb4a3828c440e100a4b2658dd2e8f422faf08f4fcc8e2c92b240656b44/cbor2-5.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1e15c3a08008cf13ce1dfc64d17c960df5d66d935788d28ec7df54bf0ffb0ef", size = 257388 }, + { url = "https://files.pythonhosted.org/packages/34/d5/252657bc5af964fc5f19c0e0e82031b4c32eba5d3ed4098e963e0e8c47a6/cbor2-5.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f6cdf7eb604ea0e7ef34e3f0b5447da0029ecd3ab7b2dc70e43fa5f7bcfca89", size = 251494 }, + { url = "https://files.pythonhosted.org/packages/8a/3a/503ea4c2977411858ca287808d077fdb4bb1fafdb4b39177b8ce3d5619ac/cbor2-5.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:dd25cbef8e8e6dbf69f0de95311aecaca7217230cda83ae99fdc37cd20d99250", size = 68147 }, + { url = "https://files.pythonhosted.org/packages/49/9e/fe4c9703fd444da193f892787110c5da2a85c16d26917fcb2584f5d00077/cbor2-5.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:40cc9c67242a7abac5a4e062bc4d1d2376979878c0565a4b2f08fd9ed9212945", size = 64126 }, + { url = "https://files.pythonhosted.org/packages/56/54/48426472f0c051982c647331441aed09b271a0500356ae0b7054c813d174/cbor2-5.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd5ca44891c06f6b85d440836c967187dc1d30b15f86f315d55c675d3a841078", size = 69031 }, + { url = "https://files.pythonhosted.org/packages/d3/68/1dd58c7706e9752188358223db58c83f3c48e07f728aa84221ffd244652f/cbor2-5.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:537d73ef930ccc1a7b6a2e8d2cbf81407d270deb18e40cda5eb511bd70f71078", size = 68825 }, + { url = "https://files.pythonhosted.org/packages/09/4e/380562fe9f9995a1875fb5ec26fd041e19d61f4630cb690a98c5195945fc/cbor2-5.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edbf814dd7763b6eda27a5770199f6ccd55bd78be8f4367092460261bfbf19d0", size = 286222 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/9eccdc1ea3c4d5c7cdb2e49b9de49534039616be5455ce69bd64c0b2efe2/cbor2-5.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fc81da8c0e09beb42923e455e477b36ff14a03b9ca18a8a2e9b462de9a953e8", size = 285688 }, + { url = "https://files.pythonhosted.org/packages/59/8c/4696d82f5bd04b3d45d9a64ec037fa242630c134e3218d6c252b4f59b909/cbor2-5.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e4a7d660d428911a3aadb7105e94438d7671ab977356fdf647a91aab751033bd", size = 277063 }, + { url = "https://files.pythonhosted.org/packages/95/50/6538e44ca970caaad2fa376b81701d073d84bf597aac07a59d0a253b1a7f/cbor2-5.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:228e0af9c0a9ddf6375b6ae010eaa1942a1901d403f134ac9ee6a76a322483f9", size = 278334 }, + { url = "https://files.pythonhosted.org/packages/64/a9/156ccd2207fb26b5b61d23728b4dbdc595d1600125aa79683a4a8ddc9313/cbor2-5.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:2d08a6c0d9ed778448e185508d870f4160ba74f59bb17a966abd0d14d0ff4dd3", size = 68404 }, + { url = "https://files.pythonhosted.org/packages/4f/49/adc53615e9dd32c4421f6935dfa2235013532c6e6b28ee515bbdd92618be/cbor2-5.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:752506cfe72da0f4014b468b30191470ee8919a64a0772bd3b36a4fccf5fcefc", size = 64047 }, + { url = "https://files.pythonhosted.org/packages/16/b1/51fb868fe38d893c570bb90b38d365ff0f00421402c1ae8f63b31b25d665/cbor2-5.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59d5da59fffe89692d5bd1530eef4d26e4eb7aa794aaa1f4e192614786409009", size = 69068 }, + { url = "https://files.pythonhosted.org/packages/b9/db/5abc62ec456f552f617aac3359a5d7114b23be9c4d886169592cd5f074b9/cbor2-5.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533117918d518e01348f8cd0331271c207e7224b9a1ed492a0ff00847f28edc8", size = 68927 }, + { url = "https://files.pythonhosted.org/packages/9a/c2/58d787395c99874d2a2395b3a22c9d48a3cfc5a7dcd5817bf74764998b75/cbor2-5.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d6d9436ff3c3323ea5863ecf7ae1139590991685b44b9eb6b7bb1734a594af6", size = 285185 }, + { url = "https://files.pythonhosted.org/packages/d0/9c/b680b264a8f4b9aa59c95e166c816275a13138cbee92dd2917f58bca47b9/cbor2-5.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:661b871ca754a619fcd98c13a38b4696b2b57dab8b24235c00b0ba322c040d24", size = 284440 }, + { url = "https://files.pythonhosted.org/packages/1f/59/68183c655d6226d0eee10027f52516882837802a8d5746317a88362ed686/cbor2-5.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8065aa90d715fd9bb28727b2d774ee16e695a0e1627ae76e54bf19f9d99d63f", size = 276876 }, + { url = "https://files.pythonhosted.org/packages/ee/a2/1964e0a569d2b81e8f4862753fee7701ae5773c22e45492a26f92f62e75a/cbor2-5.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb1b7047d73590cfe8e373e2c804fa99be47e55b1b6186602d0f86f384cecec1", size = 278216 }, + { url = "https://files.pythonhosted.org/packages/00/78/9b566d68cb88bb1ecebe354765625161c9d6060a16e55008006d6359f776/cbor2-5.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:31d511df7ebd6624fdb4cecdafb4ffb9a205f9ff8c8d98edd1bef0d27f944d74", size = 68451 }, + { url = "https://files.pythonhosted.org/packages/db/85/7a6a922d147d027fd5d8fd5224b39e8eaf152a42e8cf16351458096d3d62/cbor2-5.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:f5d37f7b0f84394d2995bd8722cb01c86a885c4821a864a34b7b4d9950c5e26e", size = 64111 }, + { url = "https://files.pythonhosted.org/packages/5f/f0/f220222a57371e33434ba7bdc25de31d611cbc0ade2a868e03c3553305e7/cbor2-5.7.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e5826e4fa4c33661960073f99cf67c82783895524fb66f3ebdd635c19b5a7d68", size = 69002 }, + { url = "https://files.pythonhosted.org/packages/c7/3c/34b62ba5173541659f248f005d13373530f02fb997b78fde00bf01ede4f4/cbor2-5.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f19a00d6ac9a77cb611073250b06bf4494b41ba78a1716704f7008e0927d9366", size = 69177 }, + { url = "https://files.pythonhosted.org/packages/77/fd/2400d820d9733df00a5c18aa74201e51d710fb91588687eb594f4a7688ea/cbor2-5.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2113aea044cd172f199da3520bc4401af69eae96c5180ca7eb660941928cb89", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/42/65/280488ef196c1d71ba123cd406ea47727bb3a0e057767a733d9793fcc428/cbor2-5.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f17eacea2d28fecf28ac413c1d7927cde0a11957487d2630655d6b5c9c46a0b", size = 281958 }, + { url = "https://files.pythonhosted.org/packages/42/82/bcdd3fdc73bd5f4194fdb08c808112010add9530bae1dcfdb1e2b2ceae19/cbor2-5.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d65deea39cae533a629561e7da672402c46731122b6129ed7c8eaa1efe04efce", size = 276025 }, + { url = "https://files.pythonhosted.org/packages/ae/a8/a6065dd6a157b877d7d8f3fe96f410fb191a2db1e6588f4d20b5f9a507c2/cbor2-5.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57d8cc29ec1fd20500748e0e767ff88c13afcee839081ba4478c41fcda6ee18b", size = 275978 }, + { url = "https://files.pythonhosted.org/packages/62/f4/37934045174af9e4253a340b43f07197af54002070cb80fae82d878f1f14/cbor2-5.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:94fb939d0946f80c49ba45105ca3a3e13e598fc9abd63efc6661b02d4b4d2c50", size = 70269 }, + { url = "https://files.pythonhosted.org/packages/0b/fd/933416643e7f5540ae818691fb23fa4189010c6efa39a12c4f59d825da28/cbor2-5.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fd7225ac820bbb9f03bd16bc1a7efb6c4d1c451f22c0a153ff4ec46495c59c5", size = 66182 }, + { url = "https://files.pythonhosted.org/packages/d5/7d/383bafeabb54c17fe5b6d5aca4e863e6b7df10bcc833b34aa169e9dfce1a/cbor2-5.7.1-py3-none-any.whl", hash = "sha256:68834e4eff2f56629ce6422b0634bc3f74c5a4269de5363f5265fe452c706ba7", size = 23829 }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709 }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814 }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467 }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280 }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454 }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609 }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586 }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290 }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663 }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964 }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064 }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792 }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198 }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262 }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "ckzg" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/e8/b262fff67d6bcaecd19c71d19ebea9184a1204e00368664e1544a2511bd8/ckzg-2.1.5.tar.gz", hash = "sha256:e48e092f9b89ebb6aaa195de2e2bb72ad2d4b35c87d3a15e4545f13c51fbbe30", size = 1123745 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/47/c52e96b5c3476524c24a8ac99002590b0dc700618e8c9ed52bfcba1acda7/ckzg-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49ee4c830de89764bfd9e8188446f3020f14d32bd4486fcbc5a4a5afad775ac0", size = 116307 }, + { url = "https://files.pythonhosted.org/packages/b5/71/a136af12f4354cd7533f52f0b5df75431824926b5cbeb5160684a1390ae9/ckzg-2.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b4f0c6c2f1a629d4d64e900c65633595c63d208001d588c61b6c8bc1b189dec", size = 99814 }, + { url = "https://files.pythonhosted.org/packages/4a/9d/97f403cb2c93abcb9f522a3b575cfd0ee9961398e23967e4ff5570844186/ckzg-2.1.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a45aaea4a42babea48bb27e387fb209f2aaaaaa16abea25a4a92a056b616f9af", size = 175697 }, + { url = "https://files.pythonhosted.org/packages/79/45/a5a414672a1daba079e43e4fb2b11d53a681b0934475f4eb32cb7babf633/ckzg-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060562273057911c39a1491e9b76055c095c10cfff1704ed70011e38b53f83d8", size = 160993 }, + { url = "https://files.pythonhosted.org/packages/6f/4b/54eb12939012294b47e5b112c1298f4fdbdbfded213926e5d404c4eae8dc/ckzg-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12a90277b17e1cb5c326c5c261dad2ebb14a7136e754593e3a0a92c94799fc1", size = 170357 }, + { url = "https://files.pythonhosted.org/packages/fc/24/6b202537c55b713a794b27f23749e569c7e4f4b9e204226edb49d5e622e1/ckzg-2.1.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:084f284d842b0a51befb2b595bf45c9c623ee3713c12500ceee9dcd05b24d14d", size = 172736 }, + { url = "https://files.pythonhosted.org/packages/53/53/df9bd835bab4edc7c030f5fc97285109d917753146d06da0a49d4982764e/ckzg-2.1.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7d42760b353c5d4a0f0d70a3161c1db75e22f4529fad4cef2228be1b8cd2d579", size = 187984 }, + { url = "https://files.pythonhosted.org/packages/f9/b6/2a319f43ceae927217949a266a9794a427aeaebcdd463fadcf3f7297ed6c/ckzg-2.1.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c547c0c61d2087f70170898948cad4d0e4583a7e25b24fdf247a426066b47bc", size = 182390 }, + { url = "https://files.pythonhosted.org/packages/c8/a5/48aaa7d84f061ec1ed0c6a20306304cc4186d3191b388c33e47548eda309/ckzg-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:2b7ef12896e2afff613f058e3bc8e3478ff626ae8a6f2d3200950304a536935f", size = 100964 }, + { url = "https://files.pythonhosted.org/packages/1e/32/d82185ecd05a91d1a35229c587eac9c518b30693c4c983ffde37e1f5a1a2/ckzg-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cead4ba760a49eaa4d7a50a0483aad9727d6103fc00c408aef15f2cd8f8dec7b", size = 116304 }, + { url = "https://files.pythonhosted.org/packages/94/a7/d22f813e032a7380f758d437348d791e8609a5c8ef2bd3654201f85a0047/ckzg-2.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3156983ba598fa05f0136325125e75197e4cf24ded255aaa6ace068cede92932", size = 99811 }, + { url = "https://files.pythonhosted.org/packages/41/62/82ee6852a629bd9a783fb7787bcc2ee6e8c00c26b4ceb821a17484081760/ckzg-2.1.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d05e2c9466b2a4214dc19da35ea4cae636e033f3434768b982d37317a0f9c520", size = 176458 }, + { url = "https://files.pythonhosted.org/packages/6d/00/5c68bb77f12eab6ba464f3955263e515ae988ddfc4bc57023b54b60feda0/ckzg-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c754bbc253cfce8814d633f135be4891e6f83a50125f418fee01323ba306f59a", size = 161841 }, + { url = "https://files.pythonhosted.org/packages/42/b3/c75079d270a7895ba6b5a95f86674d5c95f2f7e0db03328f1b21dc9a90eb/ckzg-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2b766d4aed52c8c717322f2af935da0b916bf59fbba771adb822499b45e491", size = 171101 }, + { url = "https://files.pythonhosted.org/packages/63/bf/81c533231f3bdc0029a0ec8abef9c24b2eaf929a044a77c15b6da54b91dc/ckzg-2.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dd7a296475baa5f20b5e7972448d4cb2f44d00b920d680de756c90c512af1c3b", size = 173403 }, + { url = "https://files.pythonhosted.org/packages/58/6e/bbfd04b25185c6371020dd1f1c3eb0557e36878ad6cdd78a1ea2bb47d8df/ckzg-2.1.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:97d3e93b3d94031fbd376005d86bf9b2c230ecfb4a4f4ced3b06b4aeefae6c9f", size = 188738 }, + { url = "https://files.pythonhosted.org/packages/3c/83/ab3cd495a2b37b72b4d886276b3669f101c4927b1d54b7e528aa537362ab/ckzg-2.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d2d35ba937f002b72a9a168696d0073a8e5912fa7058e77a06c370b86586401", size = 183202 }, + { url = "https://files.pythonhosted.org/packages/19/37/4ad60c2879b5e6988337b776f580b7aacb40ae6c05ef264e9835bed35e80/ckzg-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:ce2047071353ee099d44aa6575974648663204eb9b42354bfa5ac6f9b8fb63e9", size = 100965 }, + { url = "https://files.pythonhosted.org/packages/dd/9f/3ef8acd201e4d098af6bc368991ac1469a5390399abd1e78307fffb65218/ckzg-2.1.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:edead535bd9afef27b8650bba09659debd4f52638aee5ec1ab7d2c9d7e86953c", size = 116333 }, + { url = "https://files.pythonhosted.org/packages/25/c2/202947c143336185180216a4939296d824cbffca4e1438d0fe696daf1904/ckzg-2.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc78622855de3d47767cdeecfdf58fd58911f43a0fa783524e414b7e75149020", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/ac/20/ace67811fbabcfece937f8286cdd96f5668757b8944a74630b6454131545/ckzg-2.1.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094add5f197a3d278924ec1480d258f3b8b0e9f8851ae409eec83a21a738bffe", size = 176595 }, + { url = "https://files.pythonhosted.org/packages/f1/65/127fa59aae21688887249ec1caa92dabaced331de5cb4e0224216270c3d0/ckzg-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b4b05f798784400e8c4dedaf1a1d57bbbc54de790855855add876fff3c9f629", size = 162014 }, + { url = "https://files.pythonhosted.org/packages/35/de/dcaa260f6f5aca83eb9017ea0c691d3d37458e08e24dcad5efcd348d807e/ckzg-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64aef50a1cf599041b9af018bc885a3fad6a20bbaf443fc45f0457cb47914610", size = 171396 }, + { url = "https://files.pythonhosted.org/packages/c4/72/f87db164d687759ae0666a2188c5f5d11a62cac9093464efbedc1f69f4e1/ckzg-2.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0171484eedc42b9417a79e33aff3f35d48915b01c54f42c829b891947ac06551", size = 173548 }, + { url = "https://files.pythonhosted.org/packages/03/ad/b5a88a445f27dbd39eece56edffbe986bf356003bded75f79ef59e2b37c9/ckzg-2.1.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2342b98acd7b6e6e33fbbc48ccec9093e1652461daf4353115adcd708498efcd", size = 188988 }, + { url = "https://files.pythonhosted.org/packages/6e/57/42fbf29d39bd3f11a673a4e61af41b5485aa0ecf99473a0d4afc2528d24b/ckzg-2.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbce75c1e17fa60b5c33bae5069b8533cf5a4d028ef7d1f755b14a16f72307cf", size = 183513 }, + { url = "https://files.pythonhosted.org/packages/27/c0/ef4c9e9256088e5a425cedb80f26e2a0c853128571b027d8174caf97b2f6/ckzg-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:827be2aeffc8a10bfb39b8dad45def82164dfcde735818c4053f5064474ae1b4", size = 100992 }, + { url = "https://files.pythonhosted.org/packages/ba/4b/089392b6f0015bb368b453f26330c643bf0087f77835df2328a1da2af401/ckzg-2.1.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d955f4e18bb9a9b3a6f55114052edd41650c29edd5f81e417c8f01abace8207", size = 116340 }, + { url = "https://files.pythonhosted.org/packages/bb/45/4d8b70f69f0bc67e9262ec68200707d2d92a27e712cda2c163ebd4b4dcfa/ckzg-2.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c0961a685761196264aa49b1cf06e8a2b2add4d57987853d7dd7a7240dc5de7", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/c4/95/4193e4af65dc4839fa9fe07efad689fe726303b3ba62ee2f46c403458bec/ckzg-2.1.5-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:badb1c7dc6b932bed2c3f7695e1ce3e4bcc9601706136957408ac2bde5dd0892", size = 176586 }, + { url = "https://files.pythonhosted.org/packages/7d/9e/850f48cb41685f5016028dbde8f7846ce9c56bfdc2e9e0f3df1a975263fe/ckzg-2.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d92816b9babaee87bd9f23be10c07d5d07c709be184aa7ea08ddb2bcf2541c", size = 161970 }, + { url = "https://files.pythonhosted.org/packages/ca/df/a9993dc124e95eb30059c108efd83a1504709cf069d3bee0745d450262a0/ckzg-2.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cf39f9abe8b3f1a71188fb601a8589672ee40eb0671fc36d8cdf4e78f00f43f", size = 171364 }, + { url = "https://files.pythonhosted.org/packages/f9/03/78e8a723c1b832766e5698f7b39cc8dc27da95b62bc5c738a59564cb5f2c/ckzg-2.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:999df675674d8d31528fd9b9afd548e86decc86447f5555b451237e7953fd63f", size = 173571 }, + { url = "https://files.pythonhosted.org/packages/e3/64/27f96201c6d78fbdb9a0812cf45dded974c4d03d876dac11d9c764ef858f/ckzg-2.1.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c39a1c7b32ac345cc44046076fd069ad6b7e6f7bef230ef9be414c712c4453b8", size = 189014 }, + { url = "https://files.pythonhosted.org/packages/d2/6e/82177c4530265694f7ec151821c79351a07706dda4d8b23e8b37d0c122f0/ckzg-2.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4564765b0cc65929eca057241b9c030afac1dbae015f129cb60ca6abd6ff620", size = 183530 }, + { url = "https://files.pythonhosted.org/packages/4d/41/1edfbd007b0398321defeedf6ad2d9f86a73f6a99d5ca4b4944bf6f2d757/ckzg-2.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:55013b36514b8176197655b929bc53f020aa51a144331720dead2efc3793ed85", size = 100992 }, + { url = "https://files.pythonhosted.org/packages/a4/7a/9f7534c99f79465ac751b6f9b9445f90f6a0d5ab990c630d888cc9792386/ckzg-2.1.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:65960cf6feaf1b281af76efdfedecc536f52e47ec3982c1a2c58c0d1b36a391b", size = 113098 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/8beb84f852cd8a30441d9dfce66679463cfbcb59386b474f6406f12ff979/ckzg-2.1.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f97d29d2ef0bfd4ad4ded126a514ced89c30ee1a30dc5d20d68918263b883c23", size = 95813 }, + { url = "https://files.pythonhosted.org/packages/40/30/4179c4933607e4b233e50676c3d8228234fa543467f801daa04dd8f08dca/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3560a4dcd50f3b3a289c8b73657b239bbc6461eb9aa6ef5fe81a242f70591ff4", size = 126632 }, + { url = "https://files.pythonhosted.org/packages/c9/4e/c535ed740c6c949c9382d3c22253d83adf8600612358434f365bc5178c77/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13150a17d6aa76eb39d301440c02e5395540796d30e4d9ae30724ce191c50a28", size = 102843 }, + { url = "https://files.pythonhosted.org/packages/46/ff/efd63e7d9bf6651cbdd68cb28cae40bedeae3bdb8e32d4b7f73fe4ca48a2/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242e77af051028e4388df7c1ebc68897cf630cf80745f7b26ff0eb6e3ec7a78d", size = 111740 }, + { url = "https://files.pythonhosted.org/packages/22/71/18e05c16a2b7cb74c3ef4a96bee7a1d58b8b3be59db3f1b49a54f292b0b7/ckzg-2.1.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a77975d10c17f617d3a43d664d0f74eb342b2cb3deb9f20860e2e2aaa24643c1", size = 101017 }, + { url = "https://files.pythonhosted.org/packages/03/bd/4cc75266991d2420c3212614dc6e659490bcb8c1a03a638ff688cc775ff8/ckzg-2.1.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0ae260c705a82d9cf4b88eaa2e8f86263c23d99d4ec282f22838f27d24f9306c", size = 113098 }, + { url = "https://files.pythonhosted.org/packages/d6/4c/fc5e6a3463b8d8bb36d21230f41fee13d5ba3fe3f594f1c20bb9bab1fab3/ckzg-2.1.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c21b0a4ad05a9e32e715118695d7a0912b4ee73198d63cc98de4d585597627e", size = 95811 }, + { url = "https://files.pythonhosted.org/packages/6d/2e/3be3977c57a51f516f2897d543ddca7901659ae1705b5dc3dbf54e0b66f2/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0577aee9848d7a9cef750ff6f303f586caf33da986a762ca57ac0c57e59fb6d", size = 126633 }, + { url = "https://files.pythonhosted.org/packages/72/3b/8314ca493654bd035cc0a66308cec9a1089cf3bc33c2837e3e265345f3cc/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15faa1145b2408e17e3b2f0b159de325b0198615aa30268bb6cd8f4385ed745", size = 102844 }, + { url = "https://files.pythonhosted.org/packages/f4/9b/1729ee420865a71e68f18880341b37ef980c2881674dc0b06460253ef25e/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d009dba9838630183610008a81bd80aafb389d45d8293d7a2fff7a5ea82266", size = 111739 }, + { url = "https://files.pythonhosted.org/packages/40/40/f259e2bf986d39717427bc12baa8189cd43f9675e81cd3bcab639e593614/ckzg-2.1.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:df66d2be54d91f74aded4ceb71e7b1f789e2636a3015f438904a22ec9de750f1", size = 101018 }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800 }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198 }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953 }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766 }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625 }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568 }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665 }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681 }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912 }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559 }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266 }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169 }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912 }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310 }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706 }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634 }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741 }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837 }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429 }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208 }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126 }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314 }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203 }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879 }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098 }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331 }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825 }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573 }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706 }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221 }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624 }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744 }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325 }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180 }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479 }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290 }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924 }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129 }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380 }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375 }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978 }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253 }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591 }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411 }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303 }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157 }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921 }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526 }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317 }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948 }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837 }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061 }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398 }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574 }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797 }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361 }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349 }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114 }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723 }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238 }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180 }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241 }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510 }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110 }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395 }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433 }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970 }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324 }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445 }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324 }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261 }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092 }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755 }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793 }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587 }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168 }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850 }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071 }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570 }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738 }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994 }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282 }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430 }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190 }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658 }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342 }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568 }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687 }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711 }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "curve-stablecoin" +version = "2.0.0" +source = { virtual = "." } +dependencies = [ + { name = "curve-std" }, + { name = "ownership-proxy" }, + { name = "snekmate" }, + { name = "vyper" }, +] + +[package.dev-dependencies] +dev = [ + { name = "hypothesis" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-forked" }, + { name = "pytest-profiling" }, + { name = "pytest-xdist" }, + { name = "titanoboa" }, + { name = "z3-solver" }, +] + +[package.metadata] +requires-dist = [ + { name = "curve-std", git = "https://github.com/curvefi/curve-std?rev=main" }, + { name = "ownership-proxy", git = "https://github.com/curvefi/ownership-proxy?rev=main" }, + { name = "snekmate", specifier = ">=0.1.1" }, + { name = "vyper", specifier = "==0.4.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "hypothesis", specifier = ">=6.99.0" }, + { name = "pre-commit", specifier = "==4.3.0" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "pytest-forked", specifier = ">=1.6.0" }, + { name = "pytest-profiling", specifier = ">=1.8.1" }, + { name = "pytest-xdist", specifier = ">=3.5" }, + { name = "titanoboa", git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval" }, + { name = "z3-solver", specifier = ">=4.12.0" }, +] + +[[package]] +name = "curve-std" +version = "0.1.0" +source = { git = "https://github.com/curvefi/curve-std?rev=main#cc27fda59d55e3ed77bf0b5676269ffcf445f9e9" } + +[[package]] +name = "cytoolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/d4/16916f3dc20a3f5455b63c35dcb260b3716f59ce27a93586804e70e431d5/cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0", size = 642510 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7a/3244e6e3587be9abfee3b1c320e43a279831b3c3a31fe5d08c1ee6193e6b/cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644", size = 1307813 }, + { url = "https://files.pythonhosted.org/packages/32/7e/eaf504ca59addce323ef4d4ffedc2913d83c121ec19f6419bc402f7702dc/cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07", size = 985777 }, + { url = "https://files.pythonhosted.org/packages/d4/a1/ec95443f0cf4cd0dbc574fa26ac85a0442d35f3b601a90a0e3dda077f614/cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7", size = 982865 }, + { url = "https://files.pythonhosted.org/packages/a7/1b/8503604b0c0534977363fb77d371019395dfa031a216f9b1d8729d1280e4/cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750", size = 2597969 }, + { url = "https://files.pythonhosted.org/packages/4e/e5/30748da06417cb2d4bc58e380b0c11d8c6539f4e289dc1e4f4b4fc248d0e/cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484", size = 2692230 }, + { url = "https://files.pythonhosted.org/packages/d6/84/e06580b74deb97dfd3513e4e6b660c2dedc220c7653f5bd3e4f772f4d885/cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52", size = 2565243 }, + { url = "https://files.pythonhosted.org/packages/91/5e/79c0122a34c33afcb5aaee1fec35be24fe16cecefb9bb8890f2908feae56/cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5", size = 2868602 }, + { url = "https://files.pythonhosted.org/packages/3f/84/404698ff02b32292db1e39cc4a2fbdabe15164b092cc364902984c3ce0f4/cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d", size = 2905121 }, + { url = "https://files.pythonhosted.org/packages/9f/33/afad6593829ba73fc87b5ae64441e380fc937f79f24a1cda60d23cb99b8c/cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f", size = 2684382 }, + { url = "https://files.pythonhosted.org/packages/ce/86/7900013a82ca9c6cadbfb22bf50d0fbfc3b192915d2bdd9fab3f69a9afba/cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772", size = 2518183 }, + { url = "https://files.pythonhosted.org/packages/c3/4b/acf9be2953fed6a6d795fb66de37c367915037a998a5b3d3b69476cf91fe/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada", size = 2609368 }, + { url = "https://files.pythonhosted.org/packages/fd/ec/3e30455fd526f5cc37bd3dd2a0e2aafb803ae4d271e50ce53bfc30810053/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39", size = 2561458 }, + { url = "https://files.pythonhosted.org/packages/49/27/e5815c85bb18cdf95780f9596dcfd76dee910a4d635a1924648cb8a636c6/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e", size = 2578236 }, + { url = "https://files.pythonhosted.org/packages/17/db/588e266eff397670398ea335a809152e77b02ee92e0ec42091115b42f09b/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656", size = 2770523 }, + { url = "https://files.pythonhosted.org/packages/ab/ad/82be0b999c7a0a0b362cedfc183eb090b872fd42937af2d6e97d58bc70f8/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd", size = 2512909 }, + { url = "https://files.pythonhosted.org/packages/25/21/45f07ab0339a20c518bc9006100922babc397ab7ea5ef40a395db83b9cdd/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f", size = 2755345 }, + { url = "https://files.pythonhosted.org/packages/8b/a7/e530bf2b304206f79b36d793caba1ff9448348713a41bb1ad0197714a0f2/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8", size = 2617790 }, + { url = "https://files.pythonhosted.org/packages/9f/77/7f53092121d7431589344c7d65c3d43c4111547aafabb21d3ca9032d126c/cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a", size = 900209 }, + { url = "https://files.pythonhosted.org/packages/84/e4/902578658303b9bc76b1704d3ed85e6d307d311bd9fa0b919581bea56e62/cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8", size = 944802 }, + { url = "https://files.pythonhosted.org/packages/71/9f/56a7003617b4eabd8ddfb470aacc240425cbe6ddeb756adfbbaadaa175f1/cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c", size = 904835 }, + { url = "https://files.pythonhosted.org/packages/69/82/edf1d0c32b6222f2c22e5618d6db855d44eb59f9b6f22436ff963c5d0a5c/cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327", size = 1314345 }, + { url = "https://files.pythonhosted.org/packages/2d/b5/0e3c1edaa26c2bd9db90cba0ac62c85bbca84224c7ae1c2e0072c4ea64c5/cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55", size = 989259 }, + { url = "https://files.pythonhosted.org/packages/09/aa/e2b2ee9fc684867e817640764ea5807f9d25aa1e7bdba02dd4b249aab0f7/cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294", size = 986551 }, + { url = "https://files.pythonhosted.org/packages/39/9f/4e8ee41acf6674f10a9c2c9117b2f219429a5a0f09bba6135f34ca4f08a6/cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d", size = 2688378 }, + { url = "https://files.pythonhosted.org/packages/78/94/ef006f3412bc22444d855a0fc9ecb81424237fb4e5c1a1f8f5fb79ac978f/cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5", size = 2798299 }, + { url = "https://files.pythonhosted.org/packages/df/aa/365953926ee8b4f2e07df7200c0d73632155908c8867af14b2d19cc9f1f7/cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485", size = 2639311 }, + { url = "https://files.pythonhosted.org/packages/7c/ee/62beaaee7df208f22590ad07ef8875519af49c52ca39d99460b14a00f15a/cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df", size = 2979532 }, + { url = "https://files.pythonhosted.org/packages/c5/04/2211251e450bed111ada1194dc42c461da9aea441de62a01e4085ea6de9f/cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2", size = 3018632 }, + { url = "https://files.pythonhosted.org/packages/ed/a2/4a3400e4d07d3916172bf74fede08020d7b4df01595d8a97f1e9507af5ae/cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4", size = 2788579 }, + { url = "https://files.pythonhosted.org/packages/fe/82/bb88caa53a41f600e7763c517d50e2efbbe6427ea395716a92b83f44882a/cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55", size = 2593024 }, + { url = "https://files.pythonhosted.org/packages/09/a8/8b25e59570da16c7a0f173b8c6ec0aa6f3abd47fd385c007485acb459896/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec", size = 2715304 }, + { url = "https://files.pythonhosted.org/packages/d4/56/faec7696f235521b926ffdf92c102f5b029f072d28e1020364e55b084820/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb", size = 2654461 }, + { url = "https://files.pythonhosted.org/packages/aa/82/f790ed167c04b8d2a33bed30770a9b7066fc4f573321d797190e5f05685f/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139", size = 2672077 }, + { url = "https://files.pythonhosted.org/packages/d9/b3/80b8183e7eee44f45bfa3cdd3ebdadf3dd43ffc686f96d442a6c4dded45d/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b", size = 2881589 }, + { url = "https://files.pythonhosted.org/packages/8f/05/ac5ba5ddb88a3ba7ecea4bf192194a838af564d22ea7a4812cbb6bd106ce/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167", size = 2589924 }, + { url = "https://files.pythonhosted.org/packages/8e/cd/100483cae3849d24351c8333a815dc6adaf3f04912486e59386d86d9db9a/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0", size = 2868059 }, + { url = "https://files.pythonhosted.org/packages/34/6e/3a7c56b325772d39397fc3aafb4dc054273982097178b6c3917c6dad48de/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e", size = 2721692 }, + { url = "https://files.pythonhosted.org/packages/fa/ca/9fdaee32c3bc769dfb7e7991d9499136afccea67e423d097b8fb3c5acbc1/cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781", size = 899349 }, + { url = "https://files.pythonhosted.org/packages/fd/04/2ab98edeea90311e4029e1643e43d2027b54da61453292d9ea51a103ee87/cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c", size = 945831 }, + { url = "https://files.pythonhosted.org/packages/b4/8d/777d86ea6bcc68b0fc926b0ef8ab51819e2176b37aadea072aac949d5231/cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9", size = 904076 }, + { url = "https://files.pythonhosted.org/packages/c6/ec/01426224f7acf60183d3921b25e1a8e71713d3d39cb464d64ac7aace6ea6/cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514", size = 1327800 }, + { url = "https://files.pythonhosted.org/packages/b4/07/e07e8fedd332ac9626ad58bea31416dda19bfd14310731fa38b16a97e15f/cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64", size = 997118 }, + { url = "https://files.pythonhosted.org/packages/ab/72/c0f766d63ed2f9ea8dc8e1628d385d99b41fb834ce17ac3669e3f91e115d/cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9", size = 991169 }, + { url = "https://files.pythonhosted.org/packages/df/4b/1f757353d1bf33e56a7391ecc9bc49c1e529803b93a9d2f67fe5f92906fe/cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5", size = 2700680 }, + { url = "https://files.pythonhosted.org/packages/25/73/9b25bb7ed8d419b9d6ff2ae0b3d06694de79a3f98f5169a1293ff7ad3a3f/cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76", size = 2824951 }, + { url = "https://files.pythonhosted.org/packages/0c/93/9c787f7c909e75670fff467f2504725d06d8c3f51d6dfe22c55a08c8ccd4/cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688", size = 2679635 }, + { url = "https://files.pythonhosted.org/packages/50/aa/9ee92c302cccf7a41a7311b325b51ebeff25d36c1f82bdc1bbe3f58dc947/cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8", size = 2938352 }, + { url = "https://files.pythonhosted.org/packages/6a/a3/3b58c5c1692c3bacd65640d0d5c7267a7ebb76204f7507aec29de7063d2f/cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4", size = 3022121 }, + { url = "https://files.pythonhosted.org/packages/e1/93/c647bc3334355088c57351a536c2d4a83dd45f7de591fab383975e45bff9/cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363", size = 2857656 }, + { url = "https://files.pythonhosted.org/packages/b2/c2/43fea146bf4141deea959e19dcddf268c5ed759dec5c2ed4a6941d711933/cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58", size = 2551284 }, + { url = "https://files.pythonhosted.org/packages/6f/df/cdc7a81ce5cfcde7ef523143d545635fc37e80ccacce140ae58483a21da3/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b", size = 2721673 }, + { url = "https://files.pythonhosted.org/packages/45/be/f8524bb9ad8812ad375e61238dcaa3177628234d1b908ad0b74e3657cafd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b", size = 2722884 }, + { url = "https://files.pythonhosted.org/packages/23/e6/6bb8e4f9c267ad42d1ff77b6d2e4984665505afae50a216290e1d7311431/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5", size = 2685486 }, + { url = "https://files.pythonhosted.org/packages/d7/dd/88619f9c8d2b682562c0c886bbb7c35720cb83fda2ac9a41bdd14073d9bd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c", size = 2839661 }, + { url = "https://files.pythonhosted.org/packages/b8/8d/4478ebf471ee78dd496d254dc0f4ad729cd8e6ba8257de4f0a98a2838ef2/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda", size = 2547095 }, + { url = "https://files.pythonhosted.org/packages/e6/68/f1dea33367b0b3f64e199c230a14a6b6f243c189020effafd31e970ca527/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320", size = 2870901 }, + { url = "https://files.pythonhosted.org/packages/4a/9a/33591c09dfe799b8fb692cf2ad383e2c41ab6593cc960b00d1fc8a145655/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb", size = 2765422 }, + { url = "https://files.pythonhosted.org/packages/60/2b/a8aa233c9416df87f004e57ae4280bd5e1f389b4943d179f01020c6ec629/cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5", size = 901933 }, + { url = "https://files.pythonhosted.org/packages/ad/33/4c9bdf8390dc01d2617c7f11930697157164a52259b6818ddfa2f94f89f4/cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699", size = 947989 }, + { url = "https://files.pythonhosted.org/packages/35/ac/6e2708835875f5acb52318462ed296bf94ed0cb8c7cb70e62fbd03f709e3/cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8", size = 903913 }, + { url = "https://files.pythonhosted.org/packages/71/4a/b3ddb3ee44fe0045e95dd973746f93f033b6f92cce1fc3cbbe24b329943c/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945", size = 976728 }, + { url = "https://files.pythonhosted.org/packages/42/21/a3681434aa425875dd828bb515924b0f12c37a55c7d2bc5c0c5de3aeb0b4/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a", size = 986057 }, + { url = "https://files.pythonhosted.org/packages/d9/cb/efc1b29e211e0670a6953222afaac84dcbba5cb940b130c0e49858978040/cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06", size = 992632 }, + { url = "https://files.pythonhosted.org/packages/be/b0/e50621d21e939338c97faab651f58ea7fa32101226a91de79ecfb89d71e1/cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7", size = 1317534 }, + { url = "https://files.pythonhosted.org/packages/0d/6b/25aa9739b0235a5bc4c1ea293186bc6822a4c6607acfe1422423287e7400/cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b", size = 992336 }, + { url = "https://files.pythonhosted.org/packages/e1/53/5f4deb0ff958805309d135d899c764364c1e8a632ce4994bd7c45fb98df2/cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77", size = 986118 }, + { url = "https://files.pythonhosted.org/packages/1c/e3/f6255b76c8cc0debbe1c0779130777dc0434da6d9b28a90d9f76f8cb67cd/cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886", size = 2679563 }, + { url = "https://files.pythonhosted.org/packages/59/8a/acc6e39a84e930522b965586ad3a36694f9bf247b23188ee0eb47b1c9ed1/cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e", size = 2813020 }, + { url = "https://files.pythonhosted.org/packages/db/f5/0083608286ad1716eda7c41f868e85ac549f6fd6b7646993109fa0bdfd98/cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9", size = 2669312 }, + { url = "https://files.pythonhosted.org/packages/47/a8/d16080b575520fe5da00cede1ece4e0a4180ec23f88dcdc6a2f5a90a7f7f/cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b", size = 2922147 }, + { url = "https://files.pythonhosted.org/packages/7e/bc/716c9c1243701e58cad511eb3937fd550e645293c5ed1907639c5d66f194/cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de", size = 2981602 }, + { url = "https://files.pythonhosted.org/packages/14/bc/571b232996846b27f4ac0c957dc8bf60261e9b4d0d01c8d955e82329544e/cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529", size = 2830103 }, + { url = "https://files.pythonhosted.org/packages/5b/55/c594afb46ecd78e4b7e1fb92c947ed041807875661ceda73baaf61baba4f/cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286", size = 2533802 }, + { url = "https://files.pythonhosted.org/packages/93/83/1edcf95832555a78fc43b975f3ebe8ceadcc9664dd47fd33747a14df5069/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4", size = 2706071 }, + { url = "https://files.pythonhosted.org/packages/e2/df/035a408df87f25cfe3611557818b250126cd2281b2104cd88395de205583/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5", size = 2707575 }, + { url = "https://files.pythonhosted.org/packages/7a/a4/ef78e13e16e93bf695a9331321d75fbc834a088d941f1c19e6b63314e257/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5", size = 2660486 }, + { url = "https://files.pythonhosted.org/packages/30/7a/2c3d60682b26058d435416c4e90d4a94db854de5be944dfd069ed1be648a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409", size = 2819605 }, + { url = "https://files.pythonhosted.org/packages/45/92/19b722a1d83cc443fbc0c16e0dc376f8a451437890d3d9ee370358cf0709/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28", size = 2533559 }, + { url = "https://files.pythonhosted.org/packages/1d/15/fa3b7891da51115204416f14192081d3dea0eaee091f123fdc1347de8dd1/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d", size = 2839171 }, + { url = "https://files.pythonhosted.org/packages/46/40/d3519d5cd86eebebf1e8b7174ec32dfb6ecec67b48b0cfb92bf226659b5a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574", size = 2743379 }, + { url = "https://files.pythonhosted.org/packages/93/e2/a9e7511f0a13fdbefa5bf73cf8e4763878140de9453fd3e50d6ac57b6be7/cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e", size = 900844 }, + { url = "https://files.pythonhosted.org/packages/d6/a4/fb7eb403c6a4c81e5a30363f34a71adcc8bf5292dc8ea32e2440aa5668f2/cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc", size = 946461 }, + { url = "https://files.pythonhosted.org/packages/93/bb/1c8c33d353548d240bc6e8677ee8c3560ce5fa2f084e928facf7c35a6dcf/cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5", size = 902673 }, + { url = "https://files.pythonhosted.org/packages/c4/ba/4a53acc60f59030fcaf48c7766e3c4c81bd997379425aa45b129396557b5/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3", size = 1372336 }, + { url = "https://files.pythonhosted.org/packages/ac/90/f28fd8ad8319d8f5c8da69a2c29b8cf52a6d2c0161602d92b366d58926ab/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4", size = 1011930 }, + { url = "https://files.pythonhosted.org/packages/c9/95/4561c4e0ad1c944f7673d6d916405d68080f10552cfc5d69a1cf2475a9a1/cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f", size = 1020610 }, + { url = "https://files.pythonhosted.org/packages/c3/14/b2e1ffa4995ec36e1372e243411ff36325e4e6d7ffa34eb4098f5357d176/cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65", size = 2917327 }, + { url = "https://files.pythonhosted.org/packages/4a/29/7cab6c609b4514ac84cca2f7dca6c509977a8fc16d27c3a50e97f105fa6a/cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60", size = 3108951 }, + { url = "https://files.pythonhosted.org/packages/9a/71/1d1103b819458679277206ad07d78ca6b31c4bb88d6463fd193e19bfb270/cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010", size = 2807149 }, + { url = "https://files.pythonhosted.org/packages/1a/d4/3d83a05a21e7d2ed2b9e6daf489999c29934b005de9190272b8a2e3735d0/cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2", size = 3111608 }, + { url = "https://files.pythonhosted.org/packages/51/88/96f68354c3d4af68de41f0db4fe41a23b96a50a4a416636cea325490cfeb/cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148", size = 3179373 }, + { url = "https://files.pythonhosted.org/packages/ce/50/ed87a5cd8e6f27ffbb64c39e9730e18ec66c37631db2888ae711909f10c9/cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36", size = 3003120 }, + { url = "https://files.pythonhosted.org/packages/d3/a7/acde155b050d6eaa8e9c7845c98fc5fb28501568e78e83ebbf44f8855274/cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636", size = 2703225 }, + { url = "https://files.pythonhosted.org/packages/1b/b6/9d518597c5bdea626b61101e8d2ff94124787a42259dafd9f5fc396f346a/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15", size = 2956033 }, + { url = "https://files.pythonhosted.org/packages/89/7a/93e5f860926165538c85e1c5e1670ad3424f158df810f8ccd269da652138/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8", size = 2862950 }, + { url = "https://files.pythonhosted.org/packages/76/e6/99d6af00487bedc27597b54c9fcbfd5c833a69c6b7a9b9f0fff777bfc7aa/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66", size = 2861757 }, + { url = "https://files.pythonhosted.org/packages/71/ca/adfa1fb7949478135a37755cb8e88c20cd6b75c22a05f1128f05f3ab2c60/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd", size = 2979049 }, + { url = "https://files.pythonhosted.org/packages/70/4c/7bf47a03a4497d500bc73d4204e2d907771a017fa4457741b2a1d7c09319/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf", size = 2699492 }, + { url = "https://files.pythonhosted.org/packages/7e/e7/3d034b0e4817314f07aa465d5864e9b8df9d25cb260a53dd84583e491558/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4", size = 2995646 }, + { url = "https://files.pythonhosted.org/packages/c1/62/be357181c71648d9fe1d1ce91cd42c63457dcf3c158e144416fd51dced83/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb", size = 2919481 }, + { url = "https://files.pythonhosted.org/packages/62/d5/bf5434fde726c4f80cb99912b2d8e0afa1587557e2a2d7e0315eb942f2de/cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60", size = 951595 }, + { url = "https://files.pythonhosted.org/packages/64/29/39c161e9204a9715321ddea698cbd0abc317e78522c7c642363c20589e71/cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c", size = 1004445 }, + { url = "https://files.pythonhosted.org/packages/e2/5a/7cbff5e9a689f558cb0bdf277f9562b2ac51acf7cd15e055b8c3efb0e1ef/cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f", size = 926207 }, + { url = "https://files.pythonhosted.org/packages/b7/e8/297a85ba700f437c01eba962428e6ab4572f6c3e68e8ff442ce5c9d3a496/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1", size = 980613 }, + { url = "https://files.pythonhosted.org/packages/e8/d7/2b02c9d18e9cc263a0e22690f78080809f1eafe72f26b29ccc115d3bf5c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f", size = 990476 }, + { url = "https://files.pythonhosted.org/packages/89/26/b6b159d2929310fca0eff8a4989cd4b1ecbdf7c46fdff46c7a20fcae55c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16", size = 992712 }, + { url = "https://files.pythonhosted.org/packages/42/a0/f7c572aa151ed466b0fce4a327c3cc916d3ef3c82e341be59ea4b9bee9e4/cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f", size = 1322596 }, + { url = "https://files.pythonhosted.org/packages/72/7c/a55d035e20b77b6725e85c8f1a418b3a4c23967288b8b0c2d1a40f158cbe/cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0", size = 992825 }, + { url = "https://files.pythonhosted.org/packages/03/af/39d2d3db322136e12e9336a1f13bab51eab88b386bfb11f91d3faff8ba34/cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7", size = 990525 }, + { url = "https://files.pythonhosted.org/packages/a6/bd/65d7a869d307f9b10ad45c2c1cbb40b81a8d0ed1138fa17fd904f5c83298/cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e", size = 2672409 }, + { url = "https://files.pythonhosted.org/packages/2d/fb/74dfd844bfd67e810bd36e8e3903a143035447245828e7fcd7c81351d775/cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18", size = 2808477 }, + { url = "https://files.pythonhosted.org/packages/d6/1f/587686c43e31c19241ec317da66438d093523921ea7749bbc65558a30df9/cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8", size = 2636881 }, + { url = "https://files.pythonhosted.org/packages/bc/6d/90468cd34f77cb38a11af52c4dc6199efcc97a486395a21bef72e9b7602e/cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27", size = 2937315 }, + { url = "https://files.pythonhosted.org/packages/d9/50/7b92cd78c613b92e3509e6291d3fb7e0d72ebda999a8df806a96c40ca9ab/cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930", size = 2959988 }, + { url = "https://files.pythonhosted.org/packages/44/d5/34b5a28a8d9bb329f984b4c2259407ca3f501d1abeb01bacea07937d85d1/cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42", size = 2795116 }, + { url = "https://files.pythonhosted.org/packages/f5/d9/5dd829e33273ec03bdc3c812e6c3281987ae2c5c91645582f6c331544a64/cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d", size = 2535390 }, + { url = "https://files.pythonhosted.org/packages/87/1f/7f9c58068a8eec2183110df051bc6b69dd621143f84473eeb6dc1b32905a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1", size = 2704834 }, + { url = "https://files.pythonhosted.org/packages/d2/90/667def5665333575d01a65fe3ec0ca31b897895f6e3bc1a42d6ea3659369/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032", size = 2658441 }, + { url = "https://files.pythonhosted.org/packages/23/79/6615f9a14960bd29ac98b823777b6589357833f65cf1a11b5abc1587c120/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3", size = 2654766 }, + { url = "https://files.pythonhosted.org/packages/b0/99/be59c6e0ae02153ef10ae1ff0f380fb19d973c651b50cf829a731f6c9e79/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7", size = 2827649 }, + { url = "https://files.pythonhosted.org/packages/19/b7/854ddcf9f9618844108677c20d48f4611b5c636956adea0f0e85e027608f/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44", size = 2533456 }, + { url = "https://files.pythonhosted.org/packages/45/66/bfe6fbb2bdcf03c8377c8c2f542576e15f3340c905a09d78a6cb3badd39a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397", size = 2826455 }, + { url = "https://files.pythonhosted.org/packages/c3/0c/cce4047bd927e95f59e73319c02c9bc86bd3d76392e0eb9e41a1147a479c/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794", size = 2714897 }, + { url = "https://files.pythonhosted.org/packages/ac/9a/061323bb289b565802bad14fb7ab59fcd8713105df142bcf4dd9ff64f8ac/cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2", size = 901490 }, + { url = "https://files.pythonhosted.org/packages/a3/20/1f3a733d710d2a25d6f10b463bef55ada52fe6392a5d233c8d770191f48a/cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec", size = 946730 }, + { url = "https://files.pythonhosted.org/packages/f2/22/2d657db4a5d1c10a152061800f812caba9ef20d7bd2406f51a5fd800c180/cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff", size = 905722 }, + { url = "https://files.pythonhosted.org/packages/19/97/b4a8c76796a9a8b9bc90c7992840fa1589a1af8e0426562dea4ce9b384a7/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf", size = 1372606 }, + { url = "https://files.pythonhosted.org/packages/08/d4/a1bb1a32b454a2d650db8374ff3bf875ba0fc1c36e6446ec02a83b9140a1/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f", size = 1012189 }, + { url = "https://files.pythonhosted.org/packages/21/4b/2f5cbbd81588918ee7dd70cffb66731608f578a9b72166aafa991071af7d/cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485", size = 1020624 }, + { url = "https://files.pythonhosted.org/packages/f5/99/c4954dd86cd593cd776a038b36795a259b8b5c12cbab6363edf5f6d9c909/cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867", size = 2917016 }, + { url = "https://files.pythonhosted.org/packages/b2/7c/f1f70a17e272b433232bc8a27df97e46b202d6cc07e3b0d63f7f41ba0f2d/cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e", size = 3107634 }, + { url = "https://files.pythonhosted.org/packages/8f/bd/c3226a57474b4aef1f90040510cba30d0decd3515fed48dc229b37c2f898/cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5", size = 2806221 }, + { url = "https://files.pythonhosted.org/packages/c3/47/2f7bfe4aaa1e07dc9828bea228ed744faf73b26aee0c1bdf3b5520bf1909/cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30", size = 3107671 }, + { url = "https://files.pythonhosted.org/packages/4d/12/6ff3b04fbd1369d0fcd5f8b5910ba6e427e33bf113754c4c35ec3f747924/cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b", size = 3176350 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/6691d986b728e77b5d2872743ebcd962d37a2d0f7e9ad95a81b284fbf905/cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d", size = 3001173 }, + { url = "https://files.pythonhosted.org/packages/7a/cb/f59d83a5058e1198db5a1f04e4a124c94d60390e4fa89b6d2e38ee8288a0/cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3", size = 2701374 }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1ae6d28df503b0bdae094879da2072b8ba13db5919cd3798918761578411/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27", size = 2953081 }, + { url = "https://files.pythonhosted.org/packages/f4/06/d86fe811c6222dc32d3e08f5d88d2be598a6055b4d0590e7c1428d55c386/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b", size = 2862228 }, + { url = "https://files.pythonhosted.org/packages/ae/32/978ef6f42623be44a0a03ae9de875ab54aa26c7e38c5c4cd505460b0927d/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be", size = 2861971 }, + { url = "https://files.pythonhosted.org/packages/ee/f7/74c69497e756b752b359925d1feef68b91df024a4124a823740f675dacd3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b", size = 2975304 }, + { url = "https://files.pythonhosted.org/packages/5b/2b/3ce0e6889a6491f3418ad4d84ae407b8456b02169a5a1f87990dbba7433b/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629", size = 2697371 }, + { url = "https://files.pythonhosted.org/packages/15/87/c616577f0891d97860643c845f7221e95240aa589586de727e28a5eb6e52/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac", size = 2992436 }, + { url = "https://files.pythonhosted.org/packages/e7/9f/490c81bffb3428ab1fa114051fbb5ba18aaa2e2fe4da5bf4170ca524e6b3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941", size = 2917612 }, + { url = "https://files.pythonhosted.org/packages/66/35/0fec2769660ca6472bbf3317ab634675827bb706d193e3240aaf20eab961/cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad", size = 960842 }, + { url = "https://files.pythonhosted.org/packages/46/b4/b7ce3d3cd20337becfec978ecfa6d0ef64884d0cf32d44edfed8700914b9/cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162", size = 1020835 }, + { url = "https://files.pythonhosted.org/packages/2c/1f/0498009aa563a9c5d04f520aadc6e1c0942434d089d0b2f51ea986470f55/cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e", size = 927963 }, + { url = "https://files.pythonhosted.org/packages/84/32/0522207170294cf691112a93c70a8ef942f60fa9ff8e793b63b1f09cedc0/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8", size = 922014 }, + { url = "https://files.pythonhosted.org/packages/4c/49/9be2d24adaa18fa307ff14e3e43f02b2ae4b69c4ce51cee6889eb2114990/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa", size = 918134 }, + { url = "https://files.pythonhosted.org/packages/5c/b3/6a76c3b94c6c87c72ea822e7e67405be6b649c2e37778eeac7c0c0c69de8/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887", size = 981970 }, + { url = "https://files.pythonhosted.org/packages/f6/8a/606e4c7ed14aa6a86aee6ca84a2cb804754dc6c4905b8f94e09e49f1ce60/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a", size = 978877 }, + { url = "https://files.pythonhosted.org/packages/97/ec/ad474dcb1f6c1ebfdda3c2ad2edbb1af122a0e79c9ff2cb901ffb5f59662/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283", size = 964279 }, + { url = "https://files.pythonhosted.org/packages/68/8c/d245fd416c69d27d51f14d5ad62acc4ee5971088ee31c40ffe1cc109af68/cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21", size = 916630 }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, +] + +[[package]] +name = "eth-abi" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "parsimonious" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/71/d9e1380bd77fd22f98b534699af564f189b56d539cc2b9dab908d4e4c242/eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0", size = 49797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/b4/2f3982c4cbcbf5eeb6aec62df1533c0e63c653b3021ff338d44944405676/eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877", size = 28511 }, +] + +[[package]] +name = "eth-account" +version = "0.13.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitarray" }, + { name = "ckzg" }, + { name = "eth-abi" }, + { name = "eth-keyfile" }, + { name = "eth-keys" }, + { name = "eth-rlp" }, + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "pydantic" }, + { name = "rlp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/cf/20f76a29be97339c969fd765f1237154286a565a1d61be98e76bb7af946a/eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46", size = 935998 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/18/088fb250018cbe665bc2111974301b2d59f294a565aff7564c4df6878da2/eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24", size = 587452 }, +] + +[[package]] +name = "eth-bloom" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-hash", extra = ["pycryptodome"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d6/9c345601de27b536dc8b0f4c1d6cb84fe76a47de9ac02753f58deae59c86/eth_bloom-3.1.0.tar.gz", hash = "sha256:4bc918f6fde44334e92b23cfb345db961e2e3af620535cbc872444f7a143cb88", size = 9807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e5/251bda1d3f3cff230fa39e95d41141c1b44e8f55c101c5f593ccf5a31b63/eth_bloom-3.1.0-py3-none-any.whl", hash = "sha256:c96b2dd6cafa407373bca1a9d74b650378ba672d5b17f2771bf7d3c3aaa7651c", size = 5785 }, +] + +[[package]] +name = "eth-hash" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/38/577b7bc9380ef9dff0f1dffefe0c9a1ded2385e7a06c306fd95afb6f9451/eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5", size = 12227 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028 }, +] + +[package.optional-dependencies] +pycryptodome = [ + { name = "pycryptodome" }, +] + +[[package]] +name = "eth-keyfile" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/66/dd823b1537befefbbff602e2ada88f1477c5b40ec3731e3d9bc676c5f716/eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1", size = 12267 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/fc/48a586175f847dd9e05e5b8994d2fe8336098781ec2e9836a2ad94280281/eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64", size = 7510 }, +] + +[[package]] +name = "eth-keys" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/11/1ed831c50bd74f57829aa06e58bd82a809c37e070ee501c953b9ac1f1552/eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814", size = 30166 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/25/0ae00f2b0095e559d61ad3dc32171bd5a29dfd95ab04b4edd641f7c75f72/eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf", size = 20656 }, +] + +[[package]] +name = "eth-rlp" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "rlp" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ea/ad39d001fa9fed07fad66edb00af701e29b48be0ed44a3bcf58cb3adf130/eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d", size = 7720 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3b/57efe2bc2df0980680d57c01a36516cd3171d2319ceb30e675de19fc2cc5/eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47", size = 4446 }, +] + +[[package]] +name = "eth-stdlib" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e0/bc1713e0e3d2da8927b5f00291e8228447fb0f52f6411c9497693e78bb75/eth_stdlib-0.2.8.tar.gz", hash = "sha256:cda115a841142d4646e3f05818cbb08e2ca3daafc740646fa5888f9b8db08f97", size = 28067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c6/301f03a6a82dfe958d2d6593f730582494c4266e02556fd15626c8f5194a/eth_stdlib-0.2.8-py3-none-any.whl", hash = "sha256:77979707e01899c4626b78d20f05a1da6ba1dc3224c0a173259618b7970326e9", size = 37730 }, +] + +[[package]] +name = "eth-typing" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/54/62aa24b9cc708f06316167ee71c362779c8ed21fc8234a5cd94a8f53b623/eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee", size = 21806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/72/c370bbe4c53da7bf998d3523f5a0f38867654923a82192df88d0705013d3/eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d", size = 19163 }, +] + +[[package]] +name = "eth-utils" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "pydantic" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e1/ee3a8728227c3558853e63ff35bd4c449abdf5022a19601369400deacd39/eth_utils-5.3.1.tar.gz", hash = "sha256:c94e2d2abd024a9a42023b4ddc1c645814ff3d6a737b33d5cfd890ebf159c2d1", size = 123506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/4d/257cdc01ada430b8e84b9f2385c2553f33218f5b47da9adf0a616308d4b7/eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0", size = 102529 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "gprof2dot" +version = "2025.4.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/fd/cad13fa1f7a463a607176432c4affa33ea162f02f58cc36de1d40d3e6b48/gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce", size = 39536 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/ed/89d760cb25279109b89eb52975a7b5479700d3114a2421ce735bfb2e7513/gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e", size = 37555 }, +] + +[[package]] +name = "hexbytes" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/87/adf4635b4b8c050283d74e6db9a81496063229c9263e6acc1903ab79fbec/hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765", size = 8633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/e0/3b31492b1c89da3c5a846680517871455b30c54738486fc57ac79a5761bd/hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7", size = 5074 }, +] + +[[package]] +name = "hypothesis" +version = "6.142.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/0b/76a062d1d6cd68342b460c2f5627e1ad1102a3dd781acd5c096c75aca0d6/hypothesis-6.142.4.tar.gz", hash = "sha256:b3e71a84708994aa910ea47f1483ad892a7c390839959d689b2a2b07ebfd160e", size = 466047 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/9f/8010f93e175ecd996f54df9019ee8c58025fc21ed47658b0a58dd25ebe8b/hypothesis-6.142.4-py3-none-any.whl", hash = "sha256:25eecc73fadecd8b491aed822204cfe4be9c98ff5c1e8e038d181136ffc54b5b", size = 533467 }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "immutables" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/41/0ccaa6ef9943c0609ec5aa663a3b3e681c1712c1007147b84590cec706a0/immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a", size = 89008 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/78/90d9bf6d3b15d68a3fce54596146d83748526ae1a82c4533d2c29852ed0a/immutables-0.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14cb09d4f4577ad9ab8770a340dc2158e0a5ab5775cb34c75960167a31104212", size = 31229 }, + { url = "https://files.pythonhosted.org/packages/f6/3a/3730fe8c6e18600f0abc974c730cc492e91f1473f49b832e4f38f44909eb/immutables-0.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22ba593f95044ac60d2af463f3dc86cd0e223f8c51df85dff65d663d93e19f51", size = 31078 }, + { url = "https://files.pythonhosted.org/packages/f0/6f/5d79e2be956a3fd7b12d4c16b80057a965dce920b05263d45723fe86acbb/immutables-0.21-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25afc81a7bcf26c8364f85e52a14e0095344343e79493c73b0e9a765310a0bed", size = 96485 }, + { url = "https://files.pythonhosted.org/packages/f8/a0/e5d3b48710cb99ccafc0a9d15ffafde5d5997831a7035cea9dc5c61d1fc7/immutables-0.21-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac6e2868567289f88c6810f296940c328a1d38c9abc841eed04963102a27d12", size = 96809 }, + { url = "https://files.pythonhosted.org/packages/ce/eb/00c031f91f48e7196bddf6251cc87e37a6e6833ccc5c75678b1c6797d814/immutables-0.21-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ba8bca21a1d034f4577ede1e9553a681dd01199c06b563f1a8316f2623b64985", size = 95187 }, + { url = "https://files.pythonhosted.org/packages/6c/80/639ddfaf96801af6dbb893ac2d1255125e66510979fcdb68b48f74804d3f/immutables-0.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39337bfb42f83dd787a81e2d00e90efa17c4a39a9cf1210b8a50dafe32438aae", size = 95268 }, + { url = "https://files.pythonhosted.org/packages/fd/7b/02ff2e96acdb7e9e168a55e798aab9e8555c95394735c2a8e85fe2ea9911/immutables-0.21-cp310-cp310-win32.whl", hash = "sha256:b24aa98f6cdae4ba15baf3aa00e84223bafcd0d3fd7f0443474527ec951845e1", size = 30523 }, + { url = "https://files.pythonhosted.org/packages/b6/79/a8d90c5fb39c97722a644a35953b0d449c158792359a3c554444e553913c/immutables-0.21-cp310-cp310-win_amd64.whl", hash = "sha256:715f8e5f8e1c35f036f9ac62eaf8b672eec1cdc2b4f9b73864cc64eccc76661c", size = 33847 }, + { url = "https://files.pythonhosted.org/packages/85/39/2d7d54f6cf33f6fcb3f78e985142166d2ec6ff93be761a17dfee31a372cd/immutables-0.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5d780c38067047911a2e06a86ba063ba0055618ab5573c8198ef3f368e321303", size = 31227 }, + { url = "https://files.pythonhosted.org/packages/ca/4b/260f632d0fb83b8cdabb0c1fc14657a9bdead456f3399c4eb5a0c0697989/immutables-0.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9aab9d0f0016f6e0bfe7e4a4cb831ef20063da6468b1bbc71d06ef285781ee9e", size = 31081 }, + { url = "https://files.pythonhosted.org/packages/d2/8d/0f4a1d10fac2700874680dad4d1d7b0d0bf31225cb21f3a804d085bfb603/immutables-0.21-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff83390b05d3372acb9a0c928f6cc20c78e74ca20ed88eb941f84a63b65e444", size = 99334 }, + { url = "https://files.pythonhosted.org/packages/ca/fc/20f4a5d5fcd4d17b46027d7d542190f5084f3d74abc6bdc2c0fab4d3deb3/immutables-0.21-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01497713e71509c4481ffccdbe3a47b94969345f4e92f814d6626f7c0a4c304", size = 99511 }, + { url = "https://files.pythonhosted.org/packages/7e/52/f929eaee4b247b1b71823d784734e51761685d6e69d7ad000459c3f2b84d/immutables-0.21-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc7844c9fbb5bece5bfdf2bf8ea74d308f42f40b0665fd25c58abf56d7db024a", size = 97474 }, + { url = "https://files.pythonhosted.org/packages/85/b1/690aeaa4acc1db13e8356888bc929c8d0c9363c1409ffd8ab74d65d8c49c/immutables-0.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:984106fa4345efd9f96de22e9949fc97bac8598bdebee03c20b2497a88bff3b7", size = 97940 }, + { url = "https://files.pythonhosted.org/packages/dd/de/78bd53b69ee99e034c2b69f46063abf600f81417f28de6120d430f523c98/immutables-0.21-cp311-cp311-win32.whl", hash = "sha256:1bdb5200518518601377e4877d5034e7c535e9ea8a9d601ed8b0eedef0c7becd", size = 30503 }, + { url = "https://files.pythonhosted.org/packages/1f/20/999ecf398412c9ad174da083c6ffea56d027ef62cf892f695b43210c4721/immutables-0.21-cp311-cp311-win_amd64.whl", hash = "sha256:dd00c34f431c54c95e7b84bfdbdeacb4f039a6a24eb0c1f7aa4b168bb9a6ad0a", size = 34395 }, + { url = "https://files.pythonhosted.org/packages/4d/f9/0c46f600702b815182212453f5514c0070ee168b817cdf7c3767554c8489/immutables-0.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1ed262094b755903122c3c3a83ad0e0d5c3ab7887cda12b2fe878769d1ee0d", size = 31885 }, + { url = "https://files.pythonhosted.org/packages/29/34/7608d2eab6179aa47e8f59ab0fbd5b3eeb2333d78c9dc2da0de8de4ed322/immutables-0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce604f81d9d8f26e60b52ebcb56bb5c0462c8ea50fb17868487d15f048a2f13e", size = 31537 }, + { url = "https://files.pythonhosted.org/packages/f7/52/cb9e2bb7a69338155ffabbd2f993c968c750dd2d5c6c6eaa6ebb7bfcbdfa/immutables-0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48b116aaca4500398058b5a87814857a60c4cb09417fecc12d7da0f5639b73d", size = 104270 }, + { url = "https://files.pythonhosted.org/packages/0f/a4/25df835a9b9b372a4a869a8a1ac30a32199f2b3f581ad0e249f7e3d19eed/immutables-0.21-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dad7c0c74b285cc0e555ec0e97acbdc6f1862fcd16b99abd612df3243732e741", size = 104864 }, + { url = "https://files.pythonhosted.org/packages/4a/51/b548fbc657134d658e179ee8d201ae82d9049aba5c3cb2d858ed2ecb7e3f/immutables-0.21-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e44346e2221a5a676c880ca8e0e6429fa24d1a4ae562573f5c04d7f2e759b030", size = 99733 }, + { url = "https://files.pythonhosted.org/packages/47/db/d7b1e0e88faf07fe9a88579a86f58078a9a37fff871f4b3dbcf28cad9a12/immutables-0.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b10139b529a460e53fe8be699ebd848c54c8a33ebe67763bcfcc809a475a26f", size = 101698 }, + { url = "https://files.pythonhosted.org/packages/69/2d/6fe42a1a053dd8cfb9f45e91d5246522637c7287dc6bd347f67aedf7aedb/immutables-0.21-cp312-cp312-win32.whl", hash = "sha256:fc512d808662614feb17d2d92e98f611d69669a98c7af15910acf1dc72737038", size = 30977 }, + { url = "https://files.pythonhosted.org/packages/63/45/d062aca6971e99454ce3ae42a7430037227fee961644ed1f8b6c9b99e0a5/immutables-0.21-cp312-cp312-win_amd64.whl", hash = "sha256:461dcb0f58a131045155e52a2c43de6ec2fe5ba19bdced6858a3abb63cee5111", size = 35088 }, + { url = "https://files.pythonhosted.org/packages/5e/db/60da6f5a3c3f64e0b3940c4ad86e1971d9d2eb8b13a179c26eda5ec6a298/immutables-0.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79674b51aa8dd983f9ac55f7f67b433b1df84a6b4f28ab860588389a5659485b", size = 31922 }, + { url = "https://files.pythonhosted.org/packages/9b/89/5420f1d16a652024fcccc9c07d46d4157fcaf33ff37c82412c83fc16ef36/immutables-0.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93c8350f8f7d0d9693f708229d9d0578e6f3b785ce6da4bced1da97137aacfad", size = 31552 }, + { url = "https://files.pythonhosted.org/packages/d2/d0/a5fb7c164ddb298ec37537e618b70dfa30c7cae9fac01de374c36489cbc9/immutables-0.21-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:583d2a63e444ce1538cc2bda56ae1f4a1a11473dbc0377c82b516bc7eec3b81e", size = 104334 }, + { url = "https://files.pythonhosted.org/packages/f3/a5/5fda0ee4a261a85124011ac0750fec678f00e1b2d4a5502b149a3b4d86d9/immutables-0.21-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b274a52da9b106db55eceb93fc1aea858c4e6f4740189e3548e38613eafc2021", size = 104898 }, + { url = "https://files.pythonhosted.org/packages/93/fa/d46bfe92f2c66d35916344176ff87fa839aac9c16849652947e722b7a15f/immutables-0.21-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:338bede057250b33716a3e4892e15df0bf5a5ddbf1d67ead996b3e680b49ef9e", size = 99966 }, + { url = "https://files.pythonhosted.org/packages/d7/f5/2a19e2e095f7a39d8d77dcc10669734d2d99773ce00c99bdcfeeb7d714e6/immutables-0.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8781c89583b68f604cf30f0978b722165824c3075888639fde771bf1a3e12dc0", size = 101773 }, + { url = "https://files.pythonhosted.org/packages/86/80/5b6ee53f836cf2067ced997efbf2ce20890627f150c3089ea50cf607e783/immutables-0.21-cp313-cp313-win32.whl", hash = "sha256:e97ea83befad873712f283c0cccd630f70cba753e207b4868af28d5b85e9dc54", size = 30988 }, + { url = "https://files.pythonhosted.org/packages/ff/07/f623e6da78368fc0b1772f4877afbf60f34c4cc93f1a8f1006507afa21ec/immutables-0.21-cp313-cp313-win_amd64.whl", hash = "sha256:cfcb23bd898f5a4ef88692b42c51f52ca7373a35ba4dcc215060a668639eb5da", size = 35147 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151 }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/fc/d0de12343c9f132b10c7efe40951dfb6c3cfba328941ecf4c198e6bfdd78/lru_dict-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4073333894db9840f066226d50e6f914a2240711c87d60885d8c940b69a6673f", size = 17708 }, + { url = "https://files.pythonhosted.org/packages/75/56/af1cae207a5c4f1ada20a9bde92d7d953404274f499dd8fe3f4ece91eefe/lru_dict-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0ad6361e4dd63b47b2fc8eab344198f37387e1da3dcfacfee19bafac3ec9f1eb", size = 11017 }, + { url = "https://files.pythonhosted.org/packages/e9/d1/1dcaf052b4d039b85af8a8df9090c10923acc4bed448051ce791376313f3/lru_dict-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c637ab54b8cd9802fe19b260261e38820d748adf7606e34045d3c799b6dde813", size = 11322 }, + { url = "https://files.pythonhosted.org/packages/14/d4/77553cb43a2e50c3a5bb6338fe4ba3415638a99a5c8404a4ec13ab7cec52/lru_dict-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fce5f95489ca1fc158cc9fe0f4866db9cec82c2be0470926a9080570392beaf", size = 30597 }, + { url = "https://files.pythonhosted.org/packages/14/28/184d94fcd121a0dc775fa423bf05b886ae42fc081cbd693540068cf06ece/lru_dict-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2bf2e24cf5f19c3ff69bf639306e83dced273e6fa775b04e190d7f5cd16f794", size = 31871 }, + { url = "https://files.pythonhosted.org/packages/da/0e/6b49fa5fccc7b2d28fe254c48c64323741c98334e4fe41e4694fa049c208/lru_dict-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e90059f7701bef3c4da073d6e0434a9c7dc551d5adce30e6b99ef86b186f4b4a", size = 28651 }, + { url = "https://files.pythonhosted.org/packages/41/52/c3a4922421c8e5eb6fa1fdf5f56a7e01270a141a4f5f645d5ed6931b490f/lru_dict-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ecb7ae557239c64077e9b26a142eb88e63cddb104111a5122de7bebbbd00098", size = 30277 }, + { url = "https://files.pythonhosted.org/packages/f9/10/a15f70c5c36d46adba72850e64b075c6a118d2a9ee1ce7f2af2f4a419401/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6af36166d22dba851e06a13e35bbf33845d3dd88872e6aebbc8e3e7db70f4682", size = 34793 }, + { url = "https://files.pythonhosted.org/packages/56/e3/9901f9165a8c2d650bb84ae6ba371fa635e35e8b1dfb1aff2bd7be4cfd3a/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ee38d420c77eed548df47b7d74b5169a98e71c9e975596e31ab808e76d11f09", size = 33414 }, + { url = "https://files.pythonhosted.org/packages/be/27/6323b27dd42914c3ee511631d976d49247699ef0ec6fd468a5d4eef3930e/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0e1845024c31e6ff246c9eb5e6f6f1a8bb564c06f8a7d6d031220044c081090b", size = 36345 }, + { url = "https://files.pythonhosted.org/packages/76/14/b7d9009acf698e6f5d656e35776cedd3fd09755db5b09ff372d4e2667c4e/lru_dict-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ca5474b1649555d014be1104e5558a92497509021a5ba5ea6e9b492303eb66b", size = 34619 }, + { url = "https://files.pythonhosted.org/packages/96/8d/ec1813a2618b152b845e782f8bf071e3d8cd5029fd725c8248c9db0109b6/lru_dict-1.3.0-cp310-cp310-win32.whl", hash = "sha256:ebb03a9bd50c2ed86d4f72a54e0aae156d35a14075485b2127c4b01a3f4a63fa", size = 12538 }, + { url = "https://files.pythonhosted.org/packages/dc/f4/463045af7fd4cf3840029ac75174bbff7240021daa9624bdd7a47265daf6/lru_dict-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:04cda617f4e4c27009005d0a8185ef02829b14b776d2791f5c994cc9d668bc24", size = 13652 }, + { url = "https://files.pythonhosted.org/packages/a8/c9/6fac0cb67160f0efa3cc76a6a7d04d5e21a516eeb991ebba08f4f8f01ec5/lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac", size = 17750 }, + { url = "https://files.pythonhosted.org/packages/61/14/f90dee4bc547ae266dbeffd4e11611234bb6af511dea48f3bc8dac1de478/lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51", size = 11055 }, + { url = "https://files.pythonhosted.org/packages/4e/63/a0ae20525f9d52f62ac0def47935f8a2b3b6fcd2c145218b9a27fc1fb910/lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7", size = 11330 }, + { url = "https://files.pythonhosted.org/packages/e9/c6/8c2b81b61e5206910c81b712500736227289aefe4ccfb36137aa21807003/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa", size = 31793 }, + { url = "https://files.pythonhosted.org/packages/f9/d7/af9733f94df67a2e9e31ef47d4c41aff1836024f135cdbda4743eb628452/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890", size = 33090 }, + { url = "https://files.pythonhosted.org/packages/5b/6e/5b09b069a70028bcf05dbdc57a301fbe8b3bafecf916f2ed5a3065c79a71/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd", size = 29795 }, + { url = "https://files.pythonhosted.org/packages/21/92/4690daefc2602f7c3429ecf54572d37a9e3c372d370344d2185daa4d5ecc/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c", size = 31586 }, + { url = "https://files.pythonhosted.org/packages/3c/67/0a29a91087196b02f278d8765120ee4e7486f1f72a4c505fd1cd3109e627/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98", size = 36662 }, + { url = "https://files.pythonhosted.org/packages/36/54/8d56c514cd2333b652bd44c8f1962ab986cbe68e8ad7258c9e0f360cddb6/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242", size = 35118 }, + { url = "https://files.pythonhosted.org/packages/f5/9a/c7a175d10d503b86974cb07141ca175947145dd1c7370fcda86fbbcaf326/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e", size = 38198 }, + { url = "https://files.pythonhosted.org/packages/fd/59/2e5086c8e8a05a7282a824a2a37e3c45cd5714e7b83d8bc0267cb3bb5b4f/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a", size = 36542 }, + { url = "https://files.pythonhosted.org/packages/12/52/80d0a06e5f45fe7c278dd662da6ea5b39f2ff003248f448189932f6b71c2/lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4", size = 12533 }, + { url = "https://files.pythonhosted.org/packages/ce/fe/1f12f33513310860ec6d722709ec4ad8256d9dcc3385f6ae2a244e6e66f5/lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc", size = 13651 }, + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174 }, + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742 }, + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079 }, + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469 }, + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496 }, + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914 }, + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241 }, + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320 }, + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054 }, + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613 }, + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143 }, + { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653 }, + { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811 }, + { url = "https://files.pythonhosted.org/packages/78/8b/4b7af0793512af8b0d814b3b08ccecb08f313594866cfe9aabf77f642934/lru_dict-1.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f8f7824db5a64581180ab9d09842e6dd9fcdc46aac9cb592a0807cd37ea55680", size = 10060 }, + { url = "https://files.pythonhosted.org/packages/47/04/e310269b8bbb5718025d0375d8189551f10f1ef057df2b21e4bc5714fb56/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acd04b7e7b0c0c192d738df9c317093335e7282c64c9d1bb6b7ebb54674b4e24", size = 13299 }, + { url = "https://files.pythonhosted.org/packages/e2/d2/246d375c89a71637fe193f260c500537e5dc11cf3a2b5144669bfef69295/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5c20f236f27551e3f0adbf1a987673fb1e9c38d6d284502cd38f5a3845ef681", size = 13142 }, + { url = "https://files.pythonhosted.org/packages/8a/10/56fead7639a41d507eac5163a81f18c7f47a8c1feb3046d20a9c8bb56e56/lru_dict-1.3.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca3703ff03b03a1848c563bc2663d0ad813c1cd42c4d9cf75b623716d4415d9a", size = 12839 }, + { url = "https://files.pythonhosted.org/packages/fe/a4/0d68bc4007aac962386185f625d0d5180cf574e72b5279f840abde1a0e4e/lru_dict-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a9fb71ba262c6058a0017ce83d343370d0a0dbe2ae62c2eef38241ec13219330", size = 13768 }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441 }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057 }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050 }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681 }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705 }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524 }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282 }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745 }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571 }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056 }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932 }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/56/c58fe932fe0b3e70b065f1ce5759672c81ae91d00b720023ab8cd580b7a8/mkdocs_material-9.5.41.tar.gz", hash = "sha256:30fa5d459b4b8130848ecd8e1c908878345d9d8268f7ddbc31eebe88d462d97b", size = 3963765 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/bd/92771ccb61285dacfe852f96c08601f9b4830a3e1647ccc9726588b3340b/mkdocs_material-9.5.41-py3-none-any.whl", hash = "sha256:990bc138c33342b5b73e7545915ebc0136e501bfbd8e365735144f5120891d83", size = 8672461 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "ownership-proxy" +version = "0.1.0" +source = { git = "https://github.com/curvefi/ownership-proxy?rev=main#c9ca2d228920e31e675b058c20a983c896a26831" } +dependencies = [ + { name = "snekmate" }, + { name = "vyper" }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965 }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + +[[package]] +name = "py-ecc" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/96/e73075d5c885274efada2fbc5db6377022036c2f5b4b470dbcf4106e07d5/py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d", size = 51193 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/58/383335eac96d2f1aba78741c6ce128c54e7eba2ea1dc47408257d751d35c/py_ecc-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13", size = 47814 }, +] + +[[package]] +name = "py-evm" +version = "0.12.1b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cached-property" }, + { name = "ckzg" }, + { name = "eth-bloom" }, + { name = "eth-keys" }, + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "lru-dict" }, + { name = "py-ecc" }, + { name = "rlp" }, + { name = "trie" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/57/a0a84d991bac43058200245d853de9e7e819d216b56e22f66654fdf596d5/py_evm-0.12.1b1.tar.gz", hash = "sha256:7bcd9935a3ac2989c8f068b2006f136189281ebc6e279663405cb2c5397ed890", size = 851927 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/be/1e0de8a1f1dbe3911ccae2d598a2c60610e2f4984a918690b5469e8c8774/py_evm-0.12.1b1-py3-none-any.whl", hash = "sha256:015ebc8dd95925030be87ce4b3fd31e3c70df626c5ad8665fb06cd611c73eb68", size = 798589 }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152 }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348 }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033 }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142 }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384 }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237 }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898 }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197 }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600 }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740 }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685 }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362 }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 }, + { url = "https://files.pythonhosted.org/packages/9f/7c/f5b0556590e7b4e710509105e668adb55aa9470a9f0e4dea9c40a4a11ce1/pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56", size = 1705791 }, + { url = "https://files.pythonhosted.org/packages/33/38/dcc795578d610ea1aaffef4b148b8cafcfcf4d126b1e58231ddc4e475c70/pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7", size = 1780265 }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886 }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151 }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461 }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440 }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005 }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197 }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909 }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905 }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938 }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710 }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445 }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875 }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329 }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658 }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777 }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705 }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464 }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497 }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062 }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301 }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728 }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238 }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424 }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047 }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163 }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585 }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109 }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078 }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737 }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160 }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883 }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026 }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043 }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699 }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121 }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590 }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869 }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169 }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165 }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067 }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997 }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187 }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204 }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536 }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132 }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483 }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688 }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807 }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669 }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629 }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049 }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409 }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635 }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284 }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566 }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809 }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119 }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398 }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735 }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209 }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324 }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515 }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819 }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866 }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034 }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022 }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495 }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131 }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236 }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573 }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467 }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754 }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754 }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115 }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400 }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070 }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277 }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608 }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614 }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904 }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538 }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183 }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542 }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897 }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139 }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674 }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398 }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674 }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087 }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387 }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495 }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739 }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549 }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093 }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971 }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939 }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400 }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840 }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135 }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721 }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608 }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986 }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516 }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146 }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296 }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386 }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178 }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, +] + +[[package]] +name = "pytest-forked" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/c9/93ad2ba2413057ee694884b88cf7467a46c50c438977720aeac26e73fdb7/pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f", size = 9977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/af/9c0bda43e486a3c9bf1e0f876d0f241bc3f229d7d65d09331a0868db9629/pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0", size = 4897 }, +] + +[[package]] +name = "pytest-profiling" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gprof2dot" }, + { name = "pytest" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/74/806cafd6f2108d37979ec71e73b2ff7f7db88eabd19d3b79c5d6cc229c36/pytest-profiling-1.8.1.tar.gz", hash = "sha256:3f171fa69d5c82fa9aab76d66abd5f59da69135c37d6ae5bf7557f1b154cb08d", size = 33135 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/ac/c428c66241a144617a8af7a28e2e055e1438d23b949b62ac4b401a69fb79/pytest_profiling-1.8.1-py3-none-any.whl", hash = "sha256:3dd8713a96298b42d83de8f5951df3ada3e61b3e5d2a06956684175529e17aea", size = 9929 }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722 }, +] + +[[package]] +name = "regex" +version = "2025.10.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/c8/1d2160d36b11fbe0a61acb7c3c81ab032d9ec8ad888ac9e0a61b85ab99dd/regex-2025.10.23.tar.gz", hash = "sha256:8cbaf8ceb88f96ae2356d01b9adf5e6306fa42fa6f7eab6b97794e37c959ac26", size = 401266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/11/849d5d23633a77047465eaae4cc0cbf24ded7aa496c02e8b9710e28b1687/regex-2025.10.23-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:17bbcde374bef1c5fad9b131f0e28a6a24856dd90368d8c0201e2b5a69533daa", size = 487957 }, + { url = "https://files.pythonhosted.org/packages/87/12/5985386e7e3200a0d6a6417026d2c758d783a932428a5efc0a42ca1ddf74/regex-2025.10.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4e10434279cc8567f99ca6e018e9025d14f2fded2a603380b6be2090f476426", size = 290419 }, + { url = "https://files.pythonhosted.org/packages/67/cf/a8615923f962f8fdc41a3a6093a48726955e8b1993f4614b26a41d249f9b/regex-2025.10.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c9bb421cbe7012c744a5a56cf4d6c80829c72edb1a2991677299c988d6339c8", size = 288285 }, + { url = "https://files.pythonhosted.org/packages/4e/3d/6a3a1e12c86354cd0b3cbf8c3dd6acbe853609ee3b39d47ecd3ce95caf84/regex-2025.10.23-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:275cd1c2ed8c4a78ebfa489618d7aee762e8b4732da73573c3e38236ec5f65de", size = 781458 }, + { url = "https://files.pythonhosted.org/packages/46/47/76a8da004489f2700361754859e373b87a53d043de8c47f4d1583fd39d78/regex-2025.10.23-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b426ae7952f3dc1e73a86056d520bd4e5f021397484a6835902fc5648bcacce", size = 850605 }, + { url = "https://files.pythonhosted.org/packages/67/05/fa886461f97d45a6f4b209699cb994dc6d6212d6e219d29444dac5005775/regex-2025.10.23-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5cdaf5b6d37c7da1967dbe729d819461aab6a98a072feef65bbcff0a6e60649", size = 898563 }, + { url = "https://files.pythonhosted.org/packages/2d/db/3ddd8d01455f23cabad7499f4199de0df92f5e96d39633203ff9d0b592dc/regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bfeff0b08f296ab28b4332a7e03ca31c437ee78b541ebc874bbf540e5932f8d", size = 791535 }, + { url = "https://files.pythonhosted.org/packages/7c/ae/0fa5cbf41ca92b6ec3370222fcb6c68b240d68ab10e803d086c03a19fd9e/regex-2025.10.23-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f97236a67307b775f30a74ef722b64b38b7ab7ba3bb4a2508518a5de545459c", size = 782461 }, + { url = "https://files.pythonhosted.org/packages/d4/23/70af22a016df11af4def27870eb175c2c7235b72d411ecf75a4b4a422cb6/regex-2025.10.23-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be19e7de499940cd72475fb8e46ab2ecb1cf5906bebdd18a89f9329afb1df82f", size = 774583 }, + { url = "https://files.pythonhosted.org/packages/7a/ee/a54a6851f6905f33d3c4ed64e8737b1d85ed01b5724712530ddc0f9abdb1/regex-2025.10.23-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:883df76ee42d9ecb82b37ff8d01caea5895b3f49630a64d21111078bbf8ef64c", size = 845649 }, + { url = "https://files.pythonhosted.org/packages/80/7d/c3ec1cae14e01fab00e38c41ed35f47a853359e95e9c023e9a4381bb122c/regex-2025.10.23-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2e9117d1d35fc2addae6281019ecc70dc21c30014b0004f657558b91c6a8f1a7", size = 836037 }, + { url = "https://files.pythonhosted.org/packages/15/ae/45771140dd43c4d67c87b54d3728078ed6a96599d9fc7ba6825086236782/regex-2025.10.23-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ff1307f531a5d8cf5c20ea517254551ff0a8dc722193aab66c656c5a900ea68", size = 779705 }, + { url = "https://files.pythonhosted.org/packages/b8/95/074e2581760eafce7c816a352b7d3a322536e5b68c346d1a8bacd895545c/regex-2025.10.23-cp310-cp310-win32.whl", hash = "sha256:7888475787cbfee4a7cd32998eeffe9a28129fa44ae0f691b96cb3939183ef41", size = 265663 }, + { url = "https://files.pythonhosted.org/packages/f7/c7/a25f56a718847e34d3f1608c72eadeb67653bff1a0411da023dd8f4c647b/regex-2025.10.23-cp310-cp310-win_amd64.whl", hash = "sha256:ec41a905908496ce4906dab20fb103c814558db1d69afc12c2f384549c17936a", size = 277587 }, + { url = "https://files.pythonhosted.org/packages/d3/e5/63eb17c6b5deaefd93c2bbb1feae7c0a8d2157da25883a6ca2569cf7a663/regex-2025.10.23-cp310-cp310-win_arm64.whl", hash = "sha256:b2b7f19a764d5e966d5a62bf2c28a8b4093cc864c6734510bdb4aeb840aec5e6", size = 269979 }, + { url = "https://files.pythonhosted.org/packages/82/e5/74b7cd5cd76b4171f9793042045bb1726f7856dd56e582fc3e058a7a8a5e/regex-2025.10.23-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c531155bf9179345e85032052a1e5fe1a696a6abf9cea54b97e8baefff970fd", size = 487960 }, + { url = "https://files.pythonhosted.org/packages/b9/08/854fa4b3b20471d1df1c71e831b6a1aa480281e37791e52a2df9641ec5c6/regex-2025.10.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:912e9df4e89d383681268d38ad8f5780d7cccd94ba0e9aa09ca7ab7ab4f8e7eb", size = 290425 }, + { url = "https://files.pythonhosted.org/packages/ab/d3/6272b1dd3ca1271661e168762b234ad3e00dbdf4ef0c7b9b72d2d159efa7/regex-2025.10.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f375c61bfc3138b13e762fe0ae76e3bdca92497816936534a0177201666f44f", size = 288278 }, + { url = "https://files.pythonhosted.org/packages/14/8f/c7b365dd9d9bc0a36e018cb96f2ffb60d2ba8deb589a712b437f67de2920/regex-2025.10.23-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e248cc9446081119128ed002a3801f8031e0c219b5d3c64d3cc627da29ac0a33", size = 793289 }, + { url = "https://files.pythonhosted.org/packages/d4/fb/b8fbe9aa16cf0c21f45ec5a6c74b4cecbf1a1c0deb7089d4a6f83a9c1caa/regex-2025.10.23-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b52bf9282fdf401e4f4e721f0f61fc4b159b1307244517789702407dd74e38ca", size = 860321 }, + { url = "https://files.pythonhosted.org/packages/b0/81/bf41405c772324926a9bd8a640dedaa42da0e929241834dfce0733070437/regex-2025.10.23-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c084889ab2c59765a0d5ac602fd1c3c244f9b3fcc9a65fdc7ba6b74c5287490", size = 907011 }, + { url = "https://files.pythonhosted.org/packages/a4/fb/5ad6a8b92d3f88f3797b51bb4ef47499acc2d0b53d2fbe4487a892f37a73/regex-2025.10.23-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80e8eb79009bdb0936658c44ca06e2fbbca67792013e3818eea3f5f228971c2", size = 800312 }, + { url = "https://files.pythonhosted.org/packages/42/48/b4efba0168a2b57f944205d823f8e8a3a1ae6211a34508f014ec2c712f4f/regex-2025.10.23-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6f259118ba87b814a8ec475380aee5f5ae97a75852a3507cf31d055b01b5b40", size = 782839 }, + { url = "https://files.pythonhosted.org/packages/13/2a/c9efb4c6c535b0559c1fa8e431e0574d229707c9ca718600366fcfef6801/regex-2025.10.23-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b8c72a242683dcc72d37595c4f1278dfd7642b769e46700a8df11eab19dfd82", size = 854270 }, + { url = "https://files.pythonhosted.org/packages/34/2d/68eecc1bdaee020e8ba549502291c9450d90d8590d0552247c9b543ebf7b/regex-2025.10.23-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d7b7a0a3df9952f9965342159e0c1f05384c0f056a47ce8b61034f8cecbe83", size = 845771 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/a1ae499cf9b87afb47a67316bbf1037a7c681ffe447c510ed98c0aa2c01c/regex-2025.10.23-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:413bfea20a484c524858125e92b9ce6ffdd0a4b97d4ff96b5859aa119b0f1bdd", size = 788778 }, + { url = "https://files.pythonhosted.org/packages/38/f9/70765e63f5ea7d43b2b6cd4ee9d3323f16267e530fb2a420d92d991cf0fc/regex-2025.10.23-cp311-cp311-win32.whl", hash = "sha256:f76deef1f1019a17dad98f408b8f7afc4bd007cbe835ae77b737e8c7f19ae575", size = 265666 }, + { url = "https://files.pythonhosted.org/packages/9c/1a/18e9476ee1b63aaec3844d8e1cb21842dc19272c7e86d879bfc0dcc60db3/regex-2025.10.23-cp311-cp311-win_amd64.whl", hash = "sha256:59bba9f7125536f23fdab5deeea08da0c287a64c1d3acc1c7e99515809824de8", size = 277600 }, + { url = "https://files.pythonhosted.org/packages/1d/1b/c019167b1f7a8ec77251457e3ff0339ed74ca8bce1ea13138dc98309c923/regex-2025.10.23-cp311-cp311-win_arm64.whl", hash = "sha256:b103a752b6f1632ca420225718d6ed83f6a6ced3016dd0a4ab9a6825312de566", size = 269974 }, + { url = "https://files.pythonhosted.org/packages/f6/57/eeb274d83ab189d02d778851b1ac478477522a92b52edfa6e2ae9ff84679/regex-2025.10.23-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7a44d9c00f7a0a02d3b777429281376370f3d13d2c75ae74eb94e11ebcf4a7fc", size = 489187 }, + { url = "https://files.pythonhosted.org/packages/55/5c/7dad43a9b6ea88bf77e0b8b7729a4c36978e1043165034212fd2702880c6/regex-2025.10.23-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b83601f84fde939ae3478bb32a3aef36f61b58c3208d825c7e8ce1a735f143f2", size = 291122 }, + { url = "https://files.pythonhosted.org/packages/66/21/38b71e6f2818f0f4b281c8fba8d9d57cfca7b032a648fa59696e0a54376a/regex-2025.10.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec13647907bb9d15fd192bbfe89ff06612e098a5709e7d6ecabbdd8f7908fc45", size = 288797 }, + { url = "https://files.pythonhosted.org/packages/be/95/888f069c89e7729732a6d7cca37f76b44bfb53a1e35dda8a2c7b65c1b992/regex-2025.10.23-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78d76dd2957d62501084e7012ddafc5fcd406dd982b7a9ca1ea76e8eaaf73e7e", size = 798442 }, + { url = "https://files.pythonhosted.org/packages/76/70/4f903c608faf786627a8ee17c06e0067b5acade473678b69c8094b248705/regex-2025.10.23-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8668e5f067e31a47699ebb354f43aeb9c0ef136f915bd864243098524482ac43", size = 864039 }, + { url = "https://files.pythonhosted.org/packages/62/19/2df67b526bf25756c7f447dde554fc10a220fd839cc642f50857d01e4a7b/regex-2025.10.23-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a32433fe3deb4b2d8eda88790d2808fed0dc097e84f5e683b4cd4f42edef6cca", size = 912057 }, + { url = "https://files.pythonhosted.org/packages/99/14/9a39b7c9e007968411bc3c843cc14cf15437510c0a9991f080cab654fd16/regex-2025.10.23-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d97d73818c642c938db14c0668167f8d39520ca9d983604575ade3fda193afcc", size = 803374 }, + { url = "https://files.pythonhosted.org/packages/d4/f7/3495151dd3ca79949599b6d069b72a61a2c5e24fc441dccc79dcaf708fe6/regex-2025.10.23-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bca7feecc72ee33579e9f6ddf8babbe473045717a0e7dbc347099530f96e8b9a", size = 787714 }, + { url = "https://files.pythonhosted.org/packages/28/65/ee882455e051131869957ee8597faea45188c9a98c0dad724cfb302d4580/regex-2025.10.23-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7e24af51e907d7457cc4a72691ec458320b9ae67dc492f63209f01eecb09de32", size = 858392 }, + { url = "https://files.pythonhosted.org/packages/53/25/9287fef5be97529ebd3ac79d256159cb709a07eb58d4be780d1ca3885da8/regex-2025.10.23-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d10bcde58bbdf18146f3a69ec46dd03233b94a4a5632af97aa5378da3a47d288", size = 850484 }, + { url = "https://files.pythonhosted.org/packages/f3/b4/b49b88b4fea2f14dc73e5b5842755e782fc2e52f74423d6f4adc130d5880/regex-2025.10.23-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:44383bc0c933388516c2692c9a7503e1f4a67e982f20b9a29d2fb70c6494f147", size = 789634 }, + { url = "https://files.pythonhosted.org/packages/b6/3c/2f8d199d0e84e78bcd6bdc2be9b62410624f6b796e2893d1837ae738b160/regex-2025.10.23-cp312-cp312-win32.whl", hash = "sha256:6040a86f95438a0114bba16e51dfe27f1bc004fd29fe725f54a586f6d522b079", size = 266060 }, + { url = "https://files.pythonhosted.org/packages/d7/67/c35e80969f6ded306ad70b0698863310bdf36aca57ad792f45ddc0e2271f/regex-2025.10.23-cp312-cp312-win_amd64.whl", hash = "sha256:436b4c4352fe0762e3bfa34a5567079baa2ef22aa9c37cf4d128979ccfcad842", size = 276931 }, + { url = "https://files.pythonhosted.org/packages/f5/a1/4ed147de7d2b60174f758412c87fa51ada15cd3296a0ff047f4280aaa7ca/regex-2025.10.23-cp312-cp312-win_arm64.whl", hash = "sha256:f4b1b1991617055b46aff6f6db24888c1f05f4db9801349d23f09ed0714a9335", size = 270103 }, + { url = "https://files.pythonhosted.org/packages/28/c6/195a6217a43719d5a6a12cc192a22d12c40290cecfa577f00f4fb822f07d/regex-2025.10.23-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7690f95404a1293923a296981fd943cca12c31a41af9c21ba3edd06398fc193", size = 488956 }, + { url = "https://files.pythonhosted.org/packages/4c/93/181070cd1aa2fa541ff2d3afcf763ceecd4937b34c615fa92765020a6c90/regex-2025.10.23-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a32d77aeaea58a13230100dd8797ac1a84c457f3af2fdf0d81ea689d5a9105b", size = 290997 }, + { url = "https://files.pythonhosted.org/packages/b6/c5/9d37fbe3a40ed8dda78c23e1263002497540c0d1522ed75482ef6c2000f0/regex-2025.10.23-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b24b29402f264f70a3c81f45974323b41764ff7159655360543b7cabb73e7d2f", size = 288686 }, + { url = "https://files.pythonhosted.org/packages/5f/e7/db610ff9f10c2921f9b6ac0c8d8be4681b28ddd40fc0549429366967e61f/regex-2025.10.23-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563824a08c7c03d96856d84b46fdb3bbb7cfbdf79da7ef68725cda2ce169c72a", size = 798466 }, + { url = "https://files.pythonhosted.org/packages/90/10/aab883e1fa7fe2feb15ac663026e70ca0ae1411efa0c7a4a0342d9545015/regex-2025.10.23-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0ec8bdd88d2e2659c3518087ee34b37e20bd169419ffead4240a7004e8ed03b", size = 863996 }, + { url = "https://files.pythonhosted.org/packages/a2/b0/8f686dd97a51f3b37d0238cd00a6d0f9ccabe701f05b56de1918571d0d61/regex-2025.10.23-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b577601bfe1d33913fcd9276d7607bbac827c4798d9e14d04bf37d417a6c41cb", size = 912145 }, + { url = "https://files.pythonhosted.org/packages/a3/ca/639f8cd5b08797bca38fc5e7e07f76641a428cf8c7fca05894caf045aa32/regex-2025.10.23-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9f2c68ac6cb3de94eea08a437a75eaa2bd33f9e97c84836ca0b610a5804368", size = 803370 }, + { url = "https://files.pythonhosted.org/packages/0d/1e/a40725bb76959eddf8abc42a967bed6f4851b39f5ac4f20e9794d7832aa5/regex-2025.10.23-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89f8b9ea3830c79468e26b0e21c3585f69f105157c2154a36f6b7839f8afb351", size = 787767 }, + { url = "https://files.pythonhosted.org/packages/3d/d8/8ee9858062936b0f99656dce390aa667c6e7fb0c357b1b9bf76fb5e2e708/regex-2025.10.23-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:98fd84c4e4ea185b3bb5bf065261ab45867d8875032f358a435647285c722673", size = 858335 }, + { url = "https://files.pythonhosted.org/packages/d8/0a/ed5faaa63fa8e3064ab670e08061fbf09e3a10235b19630cf0cbb9e48c0a/regex-2025.10.23-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1e11d3e5887b8b096f96b4154dfb902f29c723a9556639586cd140e77e28b313", size = 850402 }, + { url = "https://files.pythonhosted.org/packages/79/14/d05f617342f4b2b4a23561da500ca2beab062bfcc408d60680e77ecaf04d/regex-2025.10.23-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f13450328a6634348d47a88367e06b64c9d84980ef6a748f717b13f8ce64e87", size = 789739 }, + { url = "https://files.pythonhosted.org/packages/f9/7b/e8ce8eef42a15f2c3461f8b3e6e924bbc86e9605cb534a393aadc8d3aff8/regex-2025.10.23-cp313-cp313-win32.whl", hash = "sha256:37be9296598a30c6a20236248cb8b2c07ffd54d095b75d3a2a2ee5babdc51df1", size = 266054 }, + { url = "https://files.pythonhosted.org/packages/71/2d/55184ed6be6473187868d2f2e6a0708195fc58270e62a22cbf26028f2570/regex-2025.10.23-cp313-cp313-win_amd64.whl", hash = "sha256:ea7a3c283ce0f06fe789365841e9174ba05f8db16e2fd6ae00a02df9572c04c0", size = 276917 }, + { url = "https://files.pythonhosted.org/packages/9c/d4/927eced0e2bd45c45839e556f987f8c8f8683268dd3c00ad327deb3b0172/regex-2025.10.23-cp313-cp313-win_arm64.whl", hash = "sha256:d9a4953575f300a7bab71afa4cd4ac061c7697c89590a2902b536783eeb49a4f", size = 270105 }, + { url = "https://files.pythonhosted.org/packages/3e/b3/95b310605285573341fc062d1d30b19a54f857530e86c805f942c4ff7941/regex-2025.10.23-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7d6606524fa77b3912c9ef52a42ef63c6cfbfc1077e9dc6296cd5da0da286044", size = 491850 }, + { url = "https://files.pythonhosted.org/packages/a4/8f/207c2cec01e34e56db1eff606eef46644a60cf1739ecd474627db90ad90b/regex-2025.10.23-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c037aadf4d64bdc38af7db3dbd34877a057ce6524eefcb2914d6d41c56f968cc", size = 292537 }, + { url = "https://files.pythonhosted.org/packages/98/3b/025240af4ada1dc0b5f10d73f3e5122d04ce7f8908ab8881e5d82b9d61b6/regex-2025.10.23-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:99018c331fb2529084a0c9b4c713dfa49fafb47c7712422e49467c13a636c656", size = 290904 }, + { url = "https://files.pythonhosted.org/packages/81/8e/104ac14e2d3450c43db18ec03e1b96b445a94ae510b60138f00ce2cb7ca1/regex-2025.10.23-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd8aba965604d70306eb90a35528f776e59112a7114a5162824d43b76fa27f58", size = 807311 }, + { url = "https://files.pythonhosted.org/packages/19/63/78aef90141b7ce0be8a18e1782f764f6997ad09de0e05251f0d2503a914a/regex-2025.10.23-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:238e67264b4013e74136c49f883734f68656adf8257bfa13b515626b31b20f8e", size = 873241 }, + { url = "https://files.pythonhosted.org/packages/b3/a8/80eb1201bb49ae4dba68a1b284b4211ed9daa8e74dc600018a10a90399fb/regex-2025.10.23-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2eb48bd9848d66fd04826382f5e8491ae633de3233a3d64d58ceb4ecfa2113a", size = 914794 }, + { url = "https://files.pythonhosted.org/packages/f0/d5/1984b6ee93281f360a119a5ca1af6a8ca7d8417861671388bf750becc29b/regex-2025.10.23-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d36591ce06d047d0c0fe2fc5f14bfbd5b4525d08a7b6a279379085e13f0e3d0e", size = 812581 }, + { url = "https://files.pythonhosted.org/packages/c4/39/11ebdc6d9927172a64ae237d16763145db6bd45ebb4055c17b88edab72a7/regex-2025.10.23-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5d4ece8628d6e364302006366cea3ee887db397faebacc5dacf8ef19e064cf8", size = 795346 }, + { url = "https://files.pythonhosted.org/packages/3b/b4/89a591bcc08b5e436af43315284bd233ba77daf0cf20e098d7af12f006c1/regex-2025.10.23-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:39a7e8083959cb1c4ff74e483eecb5a65d3b3e1d821b256e54baf61782c906c6", size = 868214 }, + { url = "https://files.pythonhosted.org/packages/3d/ff/58ba98409c1dbc8316cdb20dafbc63ed267380a07780cafecaf5012dabc9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:842d449a8fefe546f311656cf8c0d6729b08c09a185f1cad94c756210286d6a8", size = 854540 }, + { url = "https://files.pythonhosted.org/packages/9a/f2/4a9e9338d67626e2071b643f828a482712ad15889d7268e11e9a63d6f7e9/regex-2025.10.23-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d614986dc68506be8f00474f4f6960e03e4ca9883f7df47744800e7d7c08a494", size = 799346 }, + { url = "https://files.pythonhosted.org/packages/63/be/543d35c46bebf6f7bf2be538cca74d6585f25714700c36f37f01b92df551/regex-2025.10.23-cp313-cp313t-win32.whl", hash = "sha256:a5b7a26b51a9df473ec16a1934d117443a775ceb7b39b78670b2e21893c330c9", size = 268657 }, + { url = "https://files.pythonhosted.org/packages/14/9f/4dd6b7b612037158bb2c9bcaa710e6fb3c40ad54af441b9c53b3a137a9f1/regex-2025.10.23-cp313-cp313t-win_amd64.whl", hash = "sha256:ce81c5544a5453f61cb6f548ed358cfb111e3b23f3cd42d250a4077a6be2a7b6", size = 280075 }, + { url = "https://files.pythonhosted.org/packages/81/7a/5bd0672aa65d38c8da6747c17c8b441bdb53d816c569e3261013af8e83cf/regex-2025.10.23-cp313-cp313t-win_arm64.whl", hash = "sha256:e9bf7f6699f490e4e43c44757aa179dab24d1960999c84ab5c3d5377714ed473", size = 271219 }, + { url = "https://files.pythonhosted.org/packages/73/f6/0caf29fec943f201fbc8822879c99d31e59c1d51a983d9843ee5cf398539/regex-2025.10.23-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5b5cb5b6344c4c4c24b2dc87b0bfee78202b07ef7633385df70da7fcf6f7cec6", size = 488960 }, + { url = "https://files.pythonhosted.org/packages/8e/7d/ebb7085b8fa31c24ce0355107cea2b92229d9050552a01c5d291c42aecea/regex-2025.10.23-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a6ce7973384c37bdf0f371a843f95a6e6f4e1489e10e0cf57330198df72959c5", size = 290932 }, + { url = "https://files.pythonhosted.org/packages/27/41/43906867287cbb5ca4cee671c3cc8081e15deef86a8189c3aad9ac9f6b4d/regex-2025.10.23-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2ee3663f2c334959016b56e3bd0dd187cbc73f948e3a3af14c3caaa0c3035d10", size = 288766 }, + { url = "https://files.pythonhosted.org/packages/ab/9e/ea66132776700fc77a39b1056e7a5f1308032fead94507e208dc6716b7cd/regex-2025.10.23-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2003cc82a579107e70d013482acce8ba773293f2db534fb532738395c557ff34", size = 798884 }, + { url = "https://files.pythonhosted.org/packages/d5/99/aed1453687ab63819a443930770db972c5c8064421f0d9f5da9ad029f26b/regex-2025.10.23-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:182c452279365a93a9f45874f7f191ec1c51e1f1eb41bf2b16563f1a40c1da3a", size = 864768 }, + { url = "https://files.pythonhosted.org/packages/99/5d/732fe747a1304805eb3853ce6337eea16b169f7105a0d0dd9c6a5ffa9948/regex-2025.10.23-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b1249e9ff581c5b658c8f0437f883b01f1edcf424a16388591e7c05e5e9e8b0c", size = 911394 }, + { url = "https://files.pythonhosted.org/packages/5e/48/58a1f6623466522352a6efa153b9a3714fc559d9f930e9bc947b4a88a2c3/regex-2025.10.23-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b841698f93db3ccc36caa1900d2a3be281d9539b822dc012f08fc80b46a3224", size = 803145 }, + { url = "https://files.pythonhosted.org/packages/ea/f6/7dea79be2681a5574ab3fc237aa53b2c1dfd6bd2b44d4640b6c76f33f4c1/regex-2025.10.23-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:956d89e0c92d471e8f7eee73f73fdff5ed345886378c45a43175a77538a1ffe4", size = 787831 }, + { url = "https://files.pythonhosted.org/packages/3a/ad/07b76950fbbe65f88120ca2d8d845047c401450f607c99ed38862904671d/regex-2025.10.23-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5c259cb363299a0d90d63b5c0d7568ee98419861618a95ee9d91a41cb9954462", size = 859162 }, + { url = "https://files.pythonhosted.org/packages/41/87/374f3b2021b22aa6a4fc0b750d63f9721e53d1631a238f7a1c343c1cd288/regex-2025.10.23-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:185d2b18c062820b3a40d8fefa223a83f10b20a674bf6e8c4a432e8dfd844627", size = 849899 }, + { url = "https://files.pythonhosted.org/packages/12/4a/7f7bb17c5a5a9747249807210e348450dab9212a46ae6d23ebce86ba6a2b/regex-2025.10.23-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:281d87fa790049c2b7c1b4253121edd80b392b19b5a3d28dc2a77579cb2a58ec", size = 789372 }, + { url = "https://files.pythonhosted.org/packages/c9/dd/9c7728ff544fea09bbc8635e4c9e7c423b11c24f1a7a14e6ac4831466709/regex-2025.10.23-cp314-cp314-win32.whl", hash = "sha256:63b81eef3656072e4ca87c58084c7a9c2b81d41a300b157be635a8a675aacfb8", size = 271451 }, + { url = "https://files.pythonhosted.org/packages/48/f8/ef7837ff858eb74079c4804c10b0403c0b740762e6eedba41062225f7117/regex-2025.10.23-cp314-cp314-win_amd64.whl", hash = "sha256:0967c5b86f274800a34a4ed862dfab56928144d03cb18821c5153f8777947796", size = 280173 }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d576e1dbd9885bfcd83d0e90762beea48d9373a6f7ed39170f44ed22e336/regex-2025.10.23-cp314-cp314-win_arm64.whl", hash = "sha256:c70dfe58b0a00b36aa04cdb0f798bf3e0adc31747641f69e191109fd8572c9a9", size = 273206 }, + { url = "https://files.pythonhosted.org/packages/a6/d0/2025268315e8b2b7b660039824cb7765a41623e97d4cd421510925400487/regex-2025.10.23-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1f5799ea1787aa6de6c150377d11afad39a38afd033f0c5247aecb997978c422", size = 491854 }, + { url = "https://files.pythonhosted.org/packages/44/35/5681c2fec5e8b33454390af209c4353dfc44606bf06d714b0b8bd0454ffe/regex-2025.10.23-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a9639ab7540cfea45ef57d16dcbea2e22de351998d614c3ad2f9778fa3bdd788", size = 292542 }, + { url = "https://files.pythonhosted.org/packages/5d/17/184eed05543b724132e4a18149e900f5189001fcfe2d64edaae4fbaf36b4/regex-2025.10.23-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:08f52122c352eb44c3421dab78b9b73a8a77a282cc8314ae576fcaa92b780d10", size = 290903 }, + { url = "https://files.pythonhosted.org/packages/25/d0/5e3347aa0db0de382dddfa133a7b0ae72f24b4344f3989398980b44a3924/regex-2025.10.23-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebf1baebef1c4088ad5a5623decec6b52950f0e4d7a0ae4d48f0a99f8c9cb7d7", size = 807546 }, + { url = "https://files.pythonhosted.org/packages/d2/bb/40c589bbdce1be0c55e9f8159789d58d47a22014f2f820cf2b517a5cd193/regex-2025.10.23-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:16b0f1c2e2d566c562d5c384c2b492646be0a19798532fdc1fdedacc66e3223f", size = 873322 }, + { url = "https://files.pythonhosted.org/packages/fe/56/a7e40c01575ac93360e606278d359f91829781a9f7fb6e5aa435039edbda/regex-2025.10.23-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7ada5d9dceafaab92646aa00c10a9efd9b09942dd9b0d7c5a4b73db92cc7e61", size = 914855 }, + { url = "https://files.pythonhosted.org/packages/5c/4b/d55587b192763db3163c3f508b3b67b31bb6f5e7a0e08b83013d0a59500a/regex-2025.10.23-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a36b4005770044bf08edecc798f0e41a75795b9e7c9c12fe29da8d792ef870c", size = 812724 }, + { url = "https://files.pythonhosted.org/packages/33/20/18bac334955fbe99d17229f4f8e98d05e4a501ac03a442be8facbb37c304/regex-2025.10.23-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:af7b2661dcc032da1fae82069b5ebf2ac1dfcd5359ef8b35e1367bfc92181432", size = 795439 }, + { url = "https://files.pythonhosted.org/packages/67/46/c57266be9df8549c7d85deb4cb82280cb0019e46fff677534c5fa1badfa4/regex-2025.10.23-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1cb976810ac1416a67562c2e5ba0accf6f928932320fef302e08100ed681b38e", size = 868336 }, + { url = "https://files.pythonhosted.org/packages/b8/f3/bd5879e41ef8187fec5e678e94b526a93f99e7bbe0437b0f2b47f9101694/regex-2025.10.23-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:1a56a54be3897d62f54290190fbcd754bff6932934529fbf5b29933da28fcd43", size = 854567 }, + { url = "https://files.pythonhosted.org/packages/e6/57/2b6bbdbd2f24dfed5b028033aa17ad8f7d86bb28f1a892cac8b3bc89d059/regex-2025.10.23-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f3e6d202fb52c2153f532043bbcf618fd177df47b0b306741eb9b60ba96edc3", size = 799565 }, + { url = "https://files.pythonhosted.org/packages/c7/ba/a6168f542ba73b151ed81237adf6b869c7b2f7f8d51618111296674e20ee/regex-2025.10.23-cp314-cp314t-win32.whl", hash = "sha256:1fa1186966b2621b1769fd467c7b22e317e6ba2d2cdcecc42ea3089ef04a8521", size = 274428 }, + { url = "https://files.pythonhosted.org/packages/ef/a0/c84475e14a2829e9b0864ebf77c3f7da909df9d8acfe2bb540ff0072047c/regex-2025.10.23-cp314-cp314t-win_amd64.whl", hash = "sha256:08a15d40ce28362eac3e78e83d75475147869c1ff86bc93285f43b4f4431a741", size = 284140 }, + { url = "https://files.pythonhosted.org/packages/51/33/6a08ade0eee5b8ba79386869fa6f77afeb835b60510f3525db987e2fffc4/regex-2025.10.23-cp314-cp314t-win_arm64.whl", hash = "sha256:a93e97338e1c8ea2649e130dcfbe8cd69bba5e1e163834752ab64dcb4de6d5ed", size = 274497 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 }, +] + +[[package]] +name = "rlp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2d/439b0728a92964a04d9c88ea1ca9ebb128893fbbd5834faa31f987f2fd4c/rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9", size = 33429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "snekmate" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/f5/5b285024f0a18e699f2b144d9821b2572e8665331261c9078fdad6a7aa38/snekmate-0.1.2.tar.gz", hash = "sha256:8ed2c6a29b90424543d1792599c5c0b5ccf6988931b43029d1ca7e520a045038", size = 102051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/94/c7e58d2b19e05c91454ca64bdeb109969e4fac580c7eb2a50994859b1379/snekmate-0.1.2-py3-none-any.whl", hash = "sha256:df439ba25843f52e6d7e1b58000192f77cd0a5caa9a756075655ead2b66e6314", size = 120637 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "titanoboa" +version = "0.2.7" +source = { git = "https://github.com/AlbertoCentonze/titanoboa?rev=vvm-eval#71f7ad1adc799367cfd44920fb1de945500bb3f7" } +dependencies = [ + { name = "eth-abi" }, + { name = "eth-account" }, + { name = "eth-stdlib" }, + { name = "eth-typing" }, + { name = "hypothesis" }, + { name = "mkdocs-material" }, + { name = "py-evm" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "vvm" }, + { name = "vyper" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093 }, +] + +[[package]] +name = "trie" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-hash" }, + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "rlp" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/2f/5ec6be52952de47e79b1d250c00a922365a76503e3c75605c1cd890c61aa/trie-3.1.0.tar.gz", hash = "sha256:b31fd3376d6dccfe8ad13b525e233f2c268d5c48afb90a4de09672423d4b1026", size = 72800 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/79/e6e105236eb1bb0c3ac82c968c143de456a3192c17d60df2f8c528eb3323/trie-3.1.0-py3-none-any.whl", hash = "sha256:dfc3e6ac0e76f0efa900ec1bfd082f0f1ba87f95cbfd81cc12338b03f4c679c4", size = 38940 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "virtualenv" +version = "20.35.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061 }, +] + +[[package]] +name = "vvm" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/1c/3004928eaf560ed7164514cf66cdea6e84415e2a592b95bc466b568661b1/vvm-0.3.2.tar.gz", hash = "sha256:939838e6417923ef20c48d1a5f9d9fcb39e963c7e0152aaf99c966a6c332e770", size = 14164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/38/a180481ac2369696f966cfb776c71569d7c0cc2676a03daedd83b21f5617/vvm-0.3.2-py3-none-any.whl", hash = "sha256:f9f4ccad6c9a8ff6978285f005fbb086777baa160e6132d454c35d66bc94d017", size = 13385 }, +] + +[[package]] +name = "vyper" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "cbor2" }, + { name = "immutables" }, + { name = "lark" }, + { name = "packaging" }, + { name = "pycryptodome" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/d5/958145eb2f88b95a2f3d43785e41dcdc2ab5a01af5d88199248dbed33774/vyper-0.4.3.tar.gz", hash = "sha256:22a7573657401d8a3bc690d65d6b7741680007180978c3a05f9892db31d5dcf5", size = 877634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/45/ad3c7d78aaa3e50d229968bc39eaf193f9b8eee79b5f16a3bed8f35e1395/vyper-0.4.3-py3-none-any.whl", hash = "sha256:3b9671727c888363740dc678e60336759871487d0e4e9fdd973048fa9635c4fd", size = 403104 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, +] + +[[package]] +name = "z3-solver" +version = "4.15.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/60/9a924ee28cd1d12f2482834581d9024bf05110aa1098c056e847f05f7f76/z3_solver-4.15.3.0.tar.gz", hash = "sha256:78f69aebda5519bfd8af146a129f36cf4721a3c2667e80d9fe35cc9bb4d214a6", size = 4985945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/45/dd8e9d7500faa05eafa589cc8f0f0b982ce575d51b455d62dab8e19dd571/z3_solver-4.15.3.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:65335aab295ded7c0ce27c85556067087a87052389ff160777d1a1d48ef0d74f", size = 36882388 }, + { url = "https://files.pythonhosted.org/packages/54/9e/a11186061d9fead8be43bad7c75055585694124b2ccdd896ef249fe5824f/z3_solver-4.15.3.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3e62e93adff2def3537ff1ca67c3d58a6ca6d1944e0b5e774f88627b199d50e7", size = 39637842 }, + { url = "https://files.pythonhosted.org/packages/b9/0b/f15168475e5493ea44fa3c5e642903f05d2b870db71ad05662ed87a06976/z3_solver-4.15.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afd9ceb290482097474d43f08415bcc1874f433189d1449f6c1508e9c68384", size = 29056003 }, + { url = "https://files.pythonhosted.org/packages/a7/04/32a97b1f04175ec56213168ee3659709e876e3feacacb891ed3c26c1a82c/z3_solver-4.15.3.0-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:f61ef44552489077eedd7e6d9bed52ef1875decf86d66027742099a2703b1c77", size = 27074143 }, + { url = "https://files.pythonhosted.org/packages/75/77/da54076a584557ea34f20800c68f725fe61f1dd987493fcb410b4a26f99f/z3_solver-4.15.3.0-py3-none-win32.whl", hash = "sha256:0c603f6bad7423d6411adda6af55030b725e3d30f54ea91b714abcedd73b848a", size = 13123666 }, + { url = "https://files.pythonhosted.org/packages/c1/59/abc1bad8b25e9c576484ba65ca5ed225c8ed24d601ec242712f1c370b693/z3_solver-4.15.3.0-py3-none-win_amd64.whl", hash = "sha256:06abdf6c36f97c463aea827533504fd59476d015a65cf170a88bd6a53ba13ab5", size = 16203065 }, +]